From 637d804abd98e0a055f6372c750881586cb2a6f6 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Thu, 16 Oct 2014 15:05:30 +0530 Subject: [PATCH 01/10] Operations added in Production Order DocType Auto-Fetch added to operations in Production Order DocType made operations in production order read-only capacity & duration of cycle added to workstation workstation operation timings added operation master added, Time log added Time Log Fixed Test Records added to Production Order Test Recorde for over Production Time Log added Validation added to check if time log timings are within working hours of the workstation. Validation added to check if workstation is closed on time log dates. Test Case added for Time Log --- erpnext/config/manufacturing.py | 5 + .../hr/doctype/holiday_list/test_records.json | 10 +- erpnext/manufacturing/doctype/bom/bom.js | 63 ++-- erpnext/manufacturing/doctype/bom/bom.py | 19 +- .../doctype/bom/test_records.json | 10 +- .../doctype/bom_item/bom_item.json | 234 +++++++------- .../doctype/bom_operation/bom_operation.json | 146 ++++----- .../doctype/operation/__init__.py | 0 .../doctype/operation/operation.js | 14 + .../doctype/operation/operation.json | 98 ++++++ .../doctype/operation/operation.py | 14 + .../doctype/operation/test_operation.py | 10 + .../doctype/operation/test_records.json | 7 + .../production_order/production_order.js | 32 +- .../production_order/production_order.json | 53 +++- .../production_order/production_order.py | 29 +- .../production_order/test_production_order.py | 55 ++++ .../production_order_operation/__init__.py | 0 .../production_order_operation.json | 290 ++++++++++++++++++ .../production_order_operation.py | 9 + .../doctype/workstation/test_records.json | 8 +- .../doctype/workstation/test_workstation.py | 5 +- .../doctype/workstation/workstation.json | 284 +++++++++-------- .../doctype/workstation/workstation.py | 36 ++- .../workstation_operation_hours/__init__.py | 0 .../workstation_operation_hours.json | 76 +++++ .../workstation_operation_hours.py | 9 + .../doctype/time_log/test_time_log.py | 2 +- erpnext/projects/doctype/time_log/time_log.js | 33 ++ .../projects/doctype/time_log/time_log.json | 50 ++- erpnext/projects/doctype/time_log/time_log.py | 77 ++++- erpnext/setup/doctype/company/company.py | 8 +- 32 files changed, 1287 insertions(+), 399 deletions(-) create mode 100644 erpnext/manufacturing/doctype/operation/__init__.py create mode 100644 erpnext/manufacturing/doctype/operation/operation.js create mode 100644 erpnext/manufacturing/doctype/operation/operation.json create mode 100644 erpnext/manufacturing/doctype/operation/operation.py create mode 100644 erpnext/manufacturing/doctype/operation/test_operation.py create mode 100644 erpnext/manufacturing/doctype/operation/test_records.json create mode 100644 erpnext/manufacturing/doctype/production_order_operation/__init__.py create mode 100644 erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json create mode 100644 erpnext/manufacturing/doctype/production_order_operation/production_order_operation.py create mode 100644 erpnext/manufacturing/doctype/workstation_operation_hours/__init__.py create mode 100644 erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.json create mode 100644 erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.py diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index a1644a2da9..43b46381a5 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -27,6 +27,11 @@ def get_data(): "name": "Workstation", "description": _("Where manufacturing operations are carried out."), }, + { + "type": "doctype", + "name": "Operation", + "description": _("Details of the operations carried out."), + }, ] }, diff --git a/erpnext/hr/doctype/holiday_list/test_records.json b/erpnext/hr/doctype/holiday_list/test_records.json index 9ef8c8efde..1c4abe7862 100644 --- a/erpnext/hr/doctype/holiday_list/test_records.json +++ b/erpnext/hr/doctype/holiday_list/test_records.json @@ -5,11 +5,11 @@ "holiday_list_details": [ { "description": "New Year", - "doctype": "Holiday", - "holiday_date": "2013-01-01", - "parent": "_Test Holiday List", - "parentfield": "holiday_list_details", - "parenttype": "Holiday List" + "holiday_date": "2013-01-01" + }, + { + "description": "Test Holiday", + "holiday_date": "2013-02-01" } ], "holiday_list_name": "_Test Holiday List", diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 34598b662b..5c099c43e3 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -12,7 +12,7 @@ cur_frm.cscript.refresh = function(doc,dt,dn){ } cur_frm.cscript.with_operations(doc); - erpnext.bom.set_operation_no(doc); + erpnext.bom.set_operation(doc); } cur_frm.cscript.update_cost = function() { @@ -26,62 +26,41 @@ cur_frm.cscript.update_cost = function() { } cur_frm.cscript.with_operations = function(doc) { - cur_frm.fields_dict["bom_materials"].grid.set_column_disp("operation_no", doc.with_operations); - cur_frm.fields_dict["bom_materials"].grid.toggle_reqd("operation_no", doc.with_operations); + cur_frm.fields_dict["bom_materials"].grid.set_column_disp("operation", doc.with_operations); + cur_frm.fields_dict["bom_materials"].grid.toggle_reqd("operation", doc.with_operations); } -cur_frm.cscript.operation_no = function(doc, cdt, cdn) { - var child = locals[cdt][cdn]; - if(child.parentfield=="bom_operations") erpnext.bom.set_operation_no(doc); -} - -erpnext.bom.set_operation_no = function(doc) { +erpnext.bom.set_operation = function(doc) { var op_table = doc.bom_operations || []; var operations = []; for (var i=0, j=op_table.length; i end_time )""",(self.workstation_name, start_time, end_time), as_dict=1): + return 1 + + def check_workstation_for_holiday(self, from_time, to_time): + holiday_list = frappe.db.get_value("Workstation", self.workstation_name, "holiday_list") + start_date = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') + end_date = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') + msg = _("Workstation is closed on the following dates as per Holiday List:") + flag = 0 + for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s and holiday_date between + %s and %s """,(holiday_list, start_date, end_date), as_dict=1): + flag = 1 + msg = msg + "\n" + d.holiday_date + + if flag ==1: + return msg + else: + return None \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation_operation_hours/__init__.py b/erpnext/manufacturing/doctype/workstation_operation_hours/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.json b/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.json new file mode 100644 index 0000000000..8a064ca40f --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.json @@ -0,0 +1,76 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2014-10-29 13:00:43.921508", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "start_time", + "fieldtype": "Time", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Start Time", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "fieldname": "section_break_2", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "allow_on_submit": 0, + "fieldname": "end_time", + "fieldtype": "Time", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "End Time", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "modified": "2014-10-29 13:02:24.631554", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation Operation Hours", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.py b/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.py new file mode 100644 index 0000000000..dfac1f8c42 --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.py @@ -0,0 +1,9 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class WorkstationOperationHours(Document): + pass diff --git a/erpnext/projects/doctype/time_log/test_time_log.py b/erpnext/projects/doctype/time_log/test_time_log.py index 4a312adb41..bc0a9dc24b 100644 --- a/erpnext/projects/doctype/time_log/test_time_log.py +++ b/erpnext/projects/doctype/time_log/test_time_log.py @@ -16,6 +16,6 @@ class TestTimeLog(unittest.TestCase): self.assertRaises(OverlapError, ts.insert) frappe.db.sql("delete from `tabTime Log`") - + test_records = frappe.get_test_records('Time Log') test_ignore = ["Time Log Batch", "Sales Invoice"] diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index d4d109d6f9..36ca2d2ba8 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -27,3 +27,36 @@ frappe.ui.form.on("Time Log", "to_time", function(frm) { }); cur_frm.add_fetch('task','project','project'); + +$.extend(cur_frm.cscript, { + production_order: function(doc) { + if (doc.production_order){ + var operations = []; + frappe.model.with_doc("Production Order", doc.production_order, function(pro) { + doc = frappe.get_doc("Production Order",pro); + $.each(doc.production_order_operations , function(i, row){ + operations[i] = (i+1) +". "+ row.operation; + }); + frappe.meta.get_docfield("Time Log", "operation", me.frm.doc.name).options = operations.join("\n"); + refresh_field("operation"); + }) + } + }, + + operation: function(doc) { + return cur_frm.call({ + method: "erpnext.projects.doctype.time_log.time_log.get_workstation", + args: { + "production_order": doc.production_order, + "operation": doc.operation + }, + callback: function(r) { + doc.workstation = r.workstation; + } + }); + } +}); + +if (cur_frm.doc.time_log_for == "Manufacturing") { + cur_frm.cscript.onload = cur_frm.cscript.production_order; +} \ No newline at end of file diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index 0eed1fc9af..6e2706de2c 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -16,6 +16,15 @@ "read_only": 0, "reqd": 1 }, + { + "fieldname": "time_log_for", + "fieldtype": "Select", + "label": "Time Log For", + "options": "Project\nManufacturing", + "permlevel": 0, + "precision": "", + "reqd": 1 + }, { "fieldname": "from_time", "fieldtype": "Datetime", @@ -59,6 +68,7 @@ "reqd": 0 }, { + "depends_on": "eval:doc.time_log_for == 'Project'", "fieldname": "activity_type", "fieldtype": "Link", "in_list_view": 1, @@ -66,9 +76,10 @@ "options": "Activity Type", "permlevel": 0, "read_only": 0, - "reqd": 1 + "reqd": 0 }, { + "depends_on": "eval:doc.time_log_for == 'Project'", "fieldname": "task", "fieldtype": "Link", "label": "Task", @@ -76,6 +87,41 @@ "permlevel": 0, "read_only": 0 }, + { + "depends_on": "eval:doc.time_log_for == 'Manufacturing'", + "fieldname": "production_order", + "fieldtype": "Link", + "label": "Production Order", + "options": "Production Order", + "permlevel": 0, + "precision": "" + }, + { + "depends_on": "eval:doc.time_log_for == 'Manufacturing'", + "fieldname": "operation", + "fieldtype": "Select", + "label": "Operation", + "options": "", + "permlevel": 0, + "precision": "" + }, + { + "depends_on": "eval:doc.time_log_for == 'Manufacturing'", + "fieldname": "workstation", + "fieldtype": "Link", + "label": "Workstation", + "options": "Workstation", + "permlevel": 0, + "precision": "" + }, + { + "depends_on": "eval:doc.time_log_for == 'Manufacturing'", + "fieldname": "qty", + "fieldtype": "Float", + "label": "Quantity", + "permlevel": 0, + "precision": "" + }, { "fieldname": "billable", "fieldtype": "Check", @@ -151,7 +197,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2014-10-22 16:53:26.993828", + "modified": "2014-11-19 11:39:02.633802", "modified_by": "Administrator", "module": "Projects", "name": "Time Log", diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index 6678392037..84430e9e0d 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -6,7 +6,8 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cstr, comma_and +from frappe.utils import cstr, cint, comma_and + class OverlapError(frappe.ValidationError): pass @@ -19,6 +20,14 @@ class TimeLog(Document): self.set_status() self.validate_overlap() self.calculate_total_hours() + self.check_workstation_timings() + self.validate_qty() + + def on_submit(self): + self.update_production_order() + + def on_cancel(self): + self.update_production_order_on_cancel() def calculate_total_hours(self): from frappe.utils import time_diff_in_hours @@ -59,6 +68,72 @@ class TimeLog(Document): def before_update_after_submit(self): self.set_status() + def update_production_order(self): + if self.time_log_for=="Manufacturing" and self.operation: + d = self.get_qty_and_status() + required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) + if d.get('qty') == required_qty: + d['status'] = "Completed" + + dates = self.get_production_dates() + if self.from_time < dates.start_date: + dates.start_date = self.from_time + if self.to_time > dates.end_date: + dates.end_date = self.to_time + + self.production_order_update(dates, d.get('qty'), d['status']) + + def update_production_order_on_cancel(self): + if self.time_log_for=="Manufacturing" and self.operation: + d = frappe._dict() + d = self.get_qty_and_status() + dates = self.get_production_dates() + self.production_order_update(dates, d.get('qty'), d.get('status')) + + def get_qty_and_status(self): + status = "Work in Progress" + qty = cint(frappe.db.sql("""select sum(qty) as qty from `tabTime Log` where production_order = %s + and operation = %s and docstatus=1""", (self.production_order, self.operation),as_dict=1)[0].qty) + if qty == 0: + status = "Pending" + return { + "qty": qty, + "status": status + } + + def get_production_dates(self): + return frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date from `tabTime Log` + where production_order = %s and operation = %s and docstatus=1""", + (self.production_order, self.operation), as_dict=1)[0] + + def production_order_update(self, dates, qty, status): + d = self.operation.split('. ',1) + frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s, + qty_completed = %s, status = %s where idx=%s and parent=%s and operation = %s """, + (dates.start_date, dates.end_date, qty, status, d[0], self.production_order, d[1] )) + + def check_workstation_timings(self): + if self.workstation: + frappe.get_doc("Workstation", self.workstation).check_if_within_operating_hours(self.from_time, self.to_time) + + def validate_qty(self): + if self.qty == None: + self.qty=0 + required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) + completed_qty = self.get_qty_and_status().get('qty') + if (completed_qty + cint(self.qty)) > required_qty: + frappe.throw(_("Quantity cannot be greater than pending quantity that is {0}").format(required_qty)) + +@frappe.whitelist() +def get_workstation(production_order, operation): + if operation: + d = operation.split('. ',1) + idx = d[0] + operation = d[1] + + return frappe.db.sql("""select workstation from `tabProduction Order Operation` where idx=%s and + parent=%s and operation = %s""", (idx, production_order, operation), as_dict=1)[0] + @frappe.whitelist() def get_events(start, end): from frappe.desk.reportview import build_match_conditions diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 5868fd4cf8..308eb744fa 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -82,10 +82,10 @@ class Company(Document): def create_default_accounts(self): if not self.chart_of_accounts: - frappe.throw(_("Please select Chart of Accounts")) - else: - from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts - create_charts(self.chart_of_accounts, self.name) + self.chart_of_accounts = "Standard" + + from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts + create_charts(self.chart_of_accounts, self.name) frappe.db.set(self, "default_receivable_account", frappe.db.get_value("Account", {"company": self.name, "account_type": "Receivable"})) From 812cd8a8ce15e9039106aa4ad35293658465540c Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Mon, 24 Nov 2014 13:39:09 +0530 Subject: [PATCH 02/10] Prod Order Form Modified --- .../doctype/production_order/production_order.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_order/production_order.json b/erpnext/manufacturing/doctype/production_order/production_order.json index 9119602364..df89a46041 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -139,14 +139,14 @@ { "fieldname": "start_date", "fieldtype": "Datetime", - "label": "Start Date", + "label": "Production Start Date", "permlevel": 0, "precision": "" }, { "fieldname": "end_date", "fieldtype": "Datetime", - "label": "End Date", + "label": "Production End Date", "permlevel": 0, "precision": "" }, @@ -279,7 +279,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2014-11-07 15:04:01.242315", + "modified": "2014-11-24 11:13:09.639253", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order", From 5ec7542519b0679a63e478f605be0e9a436445bb Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Tue, 25 Nov 2014 18:55:42 +0530 Subject: [PATCH 03/10] Holiday List name changed and workstation filter added to time log --- .../hr/doctype/holiday_list/holiday_list.json | 6 ++-- .../hr/doctype/holiday_list/holiday_list.py | 3 -- .../production_order/production_order.js | 4 +-- .../production_order/production_order.py | 5 ++-- .../production_order/test_production_order.py | 2 +- .../production_order_operation.json | 3 +- .../doctype/workstation/test_workstation.py | 2 +- erpnext/projects/doctype/time_log/time_log.py | 30 ++++++++++++------- .../doctype/time_log/time_log_calendar.js | 9 ++++++ 9 files changed, 41 insertions(+), 23 deletions(-) diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.json b/erpnext/hr/doctype/holiday_list/holiday_list.json index 48e0844ab1..af68b47b2b 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.json +++ b/erpnext/hr/doctype/holiday_list/holiday_list.json @@ -1,5 +1,6 @@ { "allow_import": 1, + "autoname": "field:holiday_list_name", "creation": "2013-01-10 16:34:14", "docstatus": 0, "doctype": "DocType", @@ -13,7 +14,8 @@ "oldfieldname": "holiday_list_name", "oldfieldtype": "Data", "permlevel": 0, - "reqd": 1 + "reqd": 1, + "unique": 1 }, { "fieldname": "is_default", @@ -72,7 +74,7 @@ ], "icon": "icon-calendar", "idx": 1, - "modified": "2014-05-09 02:16:38.887266", + "modified": "2014-11-25 15:42:22.419054", "modified_by": "Administrator", "module": "HR", "name": "Holiday List", diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index 496c930714..b1dec8426f 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -11,9 +11,6 @@ from frappe import throw, _ from frappe.model.document import Document class HolidayList(Document): - def autoname(self): - self.name = make_autoname(self.fiscal_year + "/" + self.holiday_list_name + "/.###") - def validate(self): self.update_default_holiday_list() diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index c383cdd8a4..fbc3cc9c2d 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -58,9 +58,9 @@ $.extend(cur_frm.cscript, { bom_no: function() { return this.frm.call({ doc: this.frm.doc, - method: "get_production_order_operations", + method: "set_production_order_operations", callback: function(r) { - if(!r.exc) refresh_field("get_production_order_operations"); + if(!r.exc) refresh_field("production_order_operations"); } }); }, diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index e9e5d8bced..aa204f0e06 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -145,7 +145,7 @@ class ProductionOrder(Document): from erpnext.stock.utils import update_bin update_bin(args) - def get_production_order_operations(self): + def set_production_order_operations(self): self.set('production_order_operations', []) operations = frappe.db.sql("""select operation, opn_description, workstation, hour_rate, time_in_mins, operating_cost, fixed_cycle_cost from `tabBOM Operation` where parent = %s""", self.bom_no, as_dict=1) @@ -230,5 +230,6 @@ def make_time_log(name, operation, from_time=None, to_time=None, qty=None, proje time_log.operation= operation time_log.qty= qty time_log.workstation= workstation - time_log.calculate_total_hours() + if from_time and to_time : + time_log.calculate_total_hours() return time_log \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index fd407e6998..799cfacfae 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -67,7 +67,7 @@ class TestProductionOrder(unittest.TestCase): }) - prod_order.get_production_order_operations() + prod_order.set_production_order_operations() prod_order.production_order_operations[0].update({ "planned_start_time": "2014-11-25 00:00:00", "planned_end_time": "2014-11-25 10:00:00" diff --git a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json index b6d42c7f66..5e12c80f88 100644 --- a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json @@ -262,6 +262,7 @@ "precision": "" }, { + "allow_on_submit": 1, "fieldname": "make_time_log", "fieldtype": "Button", "label": "Make Time Log", @@ -276,7 +277,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2014-11-13 16:47:31.015973", + "modified": "2014-11-25 13:34:10.697445", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order Operation", diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 1df661b746..24d2334a24 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -7,9 +7,9 @@ import unittest test_dependencies = ["Warehouse"] test_records = frappe.get_test_records('Workstation') - class TestWorkstation(unittest.TestCase): def test_validate_timings(self): wks = frappe.get_doc("Workstation", "_Test Workstation 1") self.assertEqual(1,wks.check_workstation_for_operation_time("2013-02-01 05:00:00", "2013-02-02 20:00:00")) + self.assertEqual(None,wks.check_workstation_for_operation_time("2013-02-03 10:00:00", "2013-02-03 20:00:00")) diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index 84430e9e0d..ffbd8f315d 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -4,7 +4,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +import frappe, json from frappe import _ from frappe.utils import cstr, cint, comma_and @@ -135,23 +135,31 @@ def get_workstation(production_order, operation): parent=%s and operation = %s""", (idx, production_order, operation), as_dict=1)[0] @frappe.whitelist() -def get_events(start, end): +def get_events(start, end, filters=None): from frappe.desk.reportview import build_match_conditions if not frappe.has_permission("Time Log"): frappe.msgprint(_("No Permission"), raise_exception=1) match = build_match_conditions("Time Log") - data = frappe.db.sql("""select name, from_time, to_time, - activity_type, task, project from `tabTime Log` - where from_time between '%(start)s' and '%(end)s' or to_time between '%(start)s' and '%(end)s' - %(match)s""" % { - "start": start, - "end": end, - "match": match and (" and " + match) or "" - }, as_dict=True, update={"allDay": 0}) + + conditions = build_match_conditions("Time Log") + conditions = conditions and (" and " + conditions) or "" + if filters: + filters = json.loads(filters) + for key in filters: + if filters[key]: + conditions += " and " + key + ' = "' + filters[key].replace('"', '\"') + '"' + data = frappe.db.sql("""select name, from_time, to_time, + activity_type, task, project, production_order, workstation from `tabTime Log` + where ( from_time between %(start)s and %(end)s or to_time between %(start)s and %(end)s ) + {conditions}""".format(conditions=conditions), { + "start": start, + "end": end + }, as_dict=True, update={"allDay": 0}) + for d in data: - d.title = d.name + ": " + (d.activity_type or "[Activity Type not set]") + d.title = d.name + ": " + (d.activity_type or d.production_order or "") if d.task: d.title += " for Task: " + d.task if d.project: diff --git a/erpnext/projects/doctype/time_log/time_log_calendar.js b/erpnext/projects/doctype/time_log/time_log_calendar.js index 5b947efeeb..1808c0cfde 100644 --- a/erpnext/projects/doctype/time_log/time_log_calendar.js +++ b/erpnext/projects/doctype/time_log/time_log_calendar.js @@ -9,5 +9,14 @@ frappe.views.calendar["Time Log"] = { "title": "title", "allDay": "allDay" }, + gantt: true, + filters: [ + { + "fieldtype": "Link", + "fieldname": "workstation", + "options": "Workstation", + "label": __("Workstation") + }, + ], get_events_method: "erpnext.projects.doctype.time_log.time_log.get_events" } \ No newline at end of file From e84fa67f3099d9a0c9915ae079e8090955778523 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Thu, 27 Nov 2014 10:49:07 +0530 Subject: [PATCH 04/10] Actual operating cost, Actal operating time added to 'production order operations' - auto fetched based on 'time log' creation 'make time log' button appears only if 'production order' document is submitted server side validation added to check if 'Production Order' mentioned on 'Time Log' is in submit state test cases added to check all of the above Time Log Bug Fixed Manufacturing seetings doctype added. prod order holiday list time calculation added --- erpnext/config/manufacturing.py | 7 +- .../hr/doctype/holiday_list/test_records.json | 3 +- .../manufacturing_settings/__init__.py | 0 .../manufacturing_settings.json | 90 ++++++++++ .../manufacturing_settings.py | 9 + .../doctype/operation/test_operation.py | 2 +- .../production_order/production_order.js | 9 + .../production_order/production_order.json | 11 +- .../production_order/production_order.py | 38 ++++- .../production_order/test_production_order.py | 32 +++- .../production_order_operation.json | 156 +++++++++++------- .../doctype/workstation/test_records.json | 1 + .../doctype/workstation/workstation.js | 10 +- .../doctype/workstation/workstation.json | 3 +- .../doctype/workstation/workstation.py | 33 ++-- .../doctype/time_log/test_time_log.py | 60 +++++++ erpnext/projects/doctype/time_log/time_log.py | 66 +++++++- .../doctype/time_log/time_log_list.html | 20 ++- .../doctype/time_log/time_log_list.js | 2 +- erpnext/setup/doctype/company/company.json | 10 +- .../form_grid/production_order_grid.html | 34 ++++ .../templates/form_grid/stock_entry_grid.html | 3 +- 22 files changed, 497 insertions(+), 102 deletions(-) create mode 100644 erpnext/manufacturing/doctype/manufacturing_settings/__init__.py create mode 100644 erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json create mode 100644 erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py create mode 100644 erpnext/templates/form_grid/production_order_grid.html diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index 43b46381a5..6c915b7a81 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -25,13 +25,18 @@ def get_data(): { "type": "doctype", "name": "Workstation", - "description": _("Where manufacturing operations are carried out."), + "description": _("Where manufacturing operations are carried."), }, { "type": "doctype", "name": "Operation", "description": _("Details of the operations carried out."), }, + { + "type": "doctype", + "name": "Manufacturing Settings", + "description": _("Global settings for all manufacturing processes."), + }, ] }, diff --git a/erpnext/hr/doctype/holiday_list/test_records.json b/erpnext/hr/doctype/holiday_list/test_records.json index 1c4abe7862..34a4894947 100644 --- a/erpnext/hr/doctype/holiday_list/test_records.json +++ b/erpnext/hr/doctype/holiday_list/test_records.json @@ -1,6 +1,7 @@ [ { - "doctype": "Holiday List", + "doctype": "Holiday List", + "name": "_Test Holiday List 1", "fiscal_year": "_Test Fiscal Year 2013", "holiday_list_details": [ { diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/__init__.py b/erpnext/manufacturing/doctype/manufacturing_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json new file mode 100644 index 0000000000..48db7bc0c2 --- /dev/null +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -0,0 +1,90 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2014-11-27 14:12:07.542534", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", + "fields": [ + { + "allow_on_submit": 0, + "default": "30", + "description": "Maximum Overtime allowed against an workstation.\n( in mins )", + "fieldname": "max_overtime", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Maximum Overtime", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "default": "No", + "fieldname": "allow_production_on_holidays", + "fieldtype": "Select", + "label": "Allow Production on Holidays", + "options": "Yes\nNo", + "permlevel": 0, + "precision": "" + }, + { + "default": "30", + "description": "Delay in start time of production order operations if automatically make time logs is used.\n(in mins)", + "fieldname": "operations_start_delay", + "fieldtype": "Float", + "label": "Operations Start Delay", + "permlevel": 0, + "precision": "" + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-wrench", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "modified": "2014-12-01 15:33:00.905276", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "Manufacturing Manager", + "set_user_permissions": 0, + "submit": 0, + "write": 1 + } + ], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py new file mode 100644 index 0000000000..d40c736fd1 --- /dev/null +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class ManufacturingSettings(Document): + pass diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py index 5823f7cac5..daa450d89c 100644 --- a/erpnext/manufacturing/doctype/operation/test_operation.py +++ b/erpnext/manufacturing/doctype/operation/test_operation.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and Contributors +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors # See license.txt import frappe diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index fbc3cc9c2d..b8a2f3cb49 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -83,6 +83,15 @@ $.extend(cur_frm.cscript, { frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } }); + }, + + auto_time_log: function(doc){ + frappe.call({ + method:"erpnext.manufacturing.doctype.production_order.production_order.auto_make_time_log", + args: { + "production_order_id": doc.name + } + }); } }); diff --git a/erpnext/manufacturing/doctype/production_order/production_order.json b/erpnext/manufacturing/doctype/production_order/production_order.json index df89a46041..6d0ce9d492 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -207,6 +207,15 @@ "precision": "", "read_only": 1 }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus==1", + "fieldname": "auto_time_log", + "fieldtype": "Button", + "label": "Automatically Make Time logs", + "permlevel": 0, + "precision": "" + }, { "fieldname": "more_info", "fieldtype": "Section Break", @@ -279,7 +288,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2014-11-24 11:13:09.639253", + "modified": "2014-12-01 11:36:56.832268", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index aa204f0e06..5e4139e77e 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, json, time, datetime -from frappe.utils import flt, nowdate +from frappe.utils import flt, nowdate, now, cint, cstr from frappe import _ from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no @@ -12,6 +12,10 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no class OverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass +form_grid_templates = { + "production_order_operations": "templates/form_grid/production_order_grid.html" +} + class ProductionOrder(Document): def validate(self): if self.docstatus == 0: @@ -146,6 +150,7 @@ class ProductionOrder(Document): update_bin(args) def set_production_order_operations(self): + """Sets operations table in 'Production Order'. """ self.set('production_order_operations', []) operations = frappe.db.sql("""select operation, opn_description, workstation, hour_rate, time_in_mins, operating_cost, fixed_cycle_cost from `tabBOM Operation` where parent = %s""", self.bom_no, as_dict=1) @@ -154,9 +159,24 @@ class ProductionOrder(Document): for d in self.get('production_order_operations'): d.status = "Pending" d.qty_completed=0 + + self.auto_caluclate_production_dates() def auto_caluclate_production_dates(self): - pass + start_delay = cint(frappe.db.get_value("Manufacturing Settings", "None", "operations_start_delay")) * 60 + time = datetime.datetime.now() + datetime.timedelta(seconds= start_delay) + for d in self.get('production_order_operations'): + holiday_list = frappe.db.get_value("Workstation", d.workstation, "holiday_list") + for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s + order by holiday_date""", holiday_list, as_dict=1): + print "time date", time.date() + print "holiday ", d.holiday_date + if d.holiday_date == time.date(): + print "time IN ", time + time = time + datetime.timedelta(seconds= 24*60*60) + d.planned_start_time = time.strftime('%Y-%m-%d %H:%M:%S') + time = time + datetime.timedelta(seconds= (cint(d.time_in_mins) * 60)) + d.planned_end_time = time.strftime('%Y-%m-%d %H:%M:%S') @frappe.whitelist() def get_item_details(item): @@ -220,7 +240,7 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_time_log(name, operation, from_time=None, to_time=None, qty=None, project=None, workstation=None): +def make_time_log(name, operation, from_time, to_time, qty=None, project=None, workstation=None): time_log = frappe.new_doc("Time Log") time_log.time_log_for = 'Manufacturing' time_log.from_time = from_time @@ -232,4 +252,14 @@ def make_time_log(name, operation, from_time=None, to_time=None, qty=None, proje time_log.workstation= workstation if from_time and to_time : time_log.calculate_total_hours() - return time_log \ No newline at end of file + return time_log + +@frappe.whitelist() +def auto_make_time_log(production_order_id): + prod_order = frappe.get_doc("Production Order", production_order_id) + for d in prod_order.production_order_operations: + operation = cstr(d.idx) + ". " + d.operation + time_log = make_time_log(prod_order.name, operation, d.planned_start_time, d.planned_end_time, + prod_order.qty, prod_order.project_name, d.workstation) + time_log.save() + frappe.msgprint(_("Time Logs created.")) diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index 799cfacfae..945e986a3f 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -8,6 +8,7 @@ import frappe from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry +from erpnext.projects.doctype.time_log.time_log import OverProductionError class TestProductionOrder(unittest.TestCase): def test_planned_qty(self): @@ -60,17 +61,20 @@ class TestProductionOrder(unittest.TestCase): def test_make_time_log(self): prod_order = frappe.get_doc({ - "doctype":"Production Order", + "doctype": "Production Order", "production_item": "_Test FG Item 2", "bom_no": "BOM/_Test FG Item 2/002", - "qty": 1 + "qty": 1, + "wip_warehouse": "_Test Warehouse - _TC", + "fg_warehouse": "_Test Warehouse 1 - _TC" }) prod_order.set_production_order_operations() prod_order.production_order_operations[0].update({ "planned_start_time": "2014-11-25 00:00:00", - "planned_end_time": "2014-11-25 10:00:00" + "planned_end_time": "2014-11-25 10:00:00", + "hour_rate": 10 }) prod_order.insert() @@ -81,6 +85,8 @@ class TestProductionOrder(unittest.TestCase): from frappe.utils import cstr from frappe.utils import time_diff_in_hours + prod_order.submit() + time_log = make_time_log( prod_order.name, cstr(d.idx) + ". " + d.operation, \ d.planned_start_time, d.planned_end_time, prod_order.qty - d.qty_completed) @@ -91,6 +97,14 @@ class TestProductionOrder(unittest.TestCase): time_log.save() time_log.submit() + manufacturing_settings = frappe.get_doc({ + "doctype": "Manufacturing Settings", + "maximum_overtime": 30, + "allow_production_on_holidays": "No" + }) + + manufacturing_settings.save() + prod_order.load_from_db() self.assertEqual(prod_order.production_order_operations[0].status, "Completed") self.assertEqual(prod_order.production_order_operations[0].qty_completed, prod_order.qty) @@ -98,11 +112,17 @@ class TestProductionOrder(unittest.TestCase): self.assertEqual(prod_order.production_order_operations[0].actual_start_time, time_log.from_time) self.assertEqual(prod_order.production_order_operations[0].actual_end_time, time_log.to_time) + self.assertEqual(prod_order.production_order_operations[0].actual_operation_time, 600) + self.assertEqual(prod_order.production_order_operations[0].actual_operating_cost, 6000) + time_log.cancel() prod_order.load_from_db() - self.assertEqual(prod_order.production_order_operations[0].status,"Pending") - self.assertEqual(prod_order.production_order_operations[0].qty_completed,0) + self.assertEqual(prod_order.production_order_operations[0].status, "Pending") + self.assertEqual(prod_order.production_order_operations[0].qty_completed, 0) + + self.assertEqual(prod_order.production_order_operations[0].actual_operation_time, 0) + self.assertEqual(prod_order.production_order_operations[0].actual_operating_cost, 0) time_log2 = frappe.copy_doc(time_log) time_log2.update({ @@ -111,6 +131,6 @@ class TestProductionOrder(unittest.TestCase): "to_time": "2014-11-26 00:00:00", "docstatus": 0 }) - self.assertRaises(frappe.ValidationError, time_log2.save) + self.assertRaises(OverProductionError, time_log2.save) test_records = frappe.get_test_records('Production Order') diff --git a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json index 5e12c80f88..5b186b7e88 100644 --- a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json @@ -45,7 +45,7 @@ "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, - "in_list_view": 1, + "in_list_view": 0, "label": "Operation Description", "no_copy": 0, "oldfieldname": "opn_description", @@ -83,19 +83,22 @@ "default": "Pending", "fieldname": "status", "fieldtype": "Select", - "in_list_view": 1, + "in_list_view": 0, "label": "Status", "options": "Pending\nWork in Progress\nCompleted", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "default": "0", "fieldname": "qty_completed", "fieldtype": "Float", + "in_list_view": 1, "label": "Qty Completed", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "allow_on_submit": 0, @@ -121,9 +124,9 @@ "unique": 0 }, { - "fieldname": "cost", + "fieldname": "estimated_time_and_cost", "fieldtype": "Section Break", - "label": "Cost", + "label": "Estimated Time and Cost", "permlevel": 0, "precision": "" }, @@ -149,57 +152,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_on_submit": 0, - "fieldname": "time_in_mins", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Operation Time (mins)", - "no_copy": 0, - "oldfieldname": "time_in_mins", - "oldfieldtype": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break", - "permlevel": 0, - "precision": "" - }, - { - "allow_on_submit": 0, - "description": "Hour rate * hours", - "fieldname": "operating_cost", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Operating Cost", - "no_copy": 0, - "oldfieldname": "operating_cost", - "oldfieldtype": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_on_submit": 0, "fieldname": "fixed_cycle_cost", @@ -221,26 +173,98 @@ "unique": 0 }, { - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "label": "Time", + "allow_on_submit": 0, + "description": "Hour Rate * Operating Time", + "fieldname": "operating_cost", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Operating Cost", + "no_copy": 0, + "oldfieldname": "operating_cost", + "oldfieldtype": "Currency", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break", "permlevel": 0, "precision": "" }, + { + "allow_on_submit": 0, + "description": "in Minutes", + "fieldname": "time_in_mins", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Operation Time", + "no_copy": 0, + "oldfieldname": "time_in_mins", + "oldfieldtype": "Currency", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "fieldname": "planned_start_time", "fieldtype": "Datetime", "label": "Planned Start Time", "permlevel": 0, - "precision": "" + "precision": "", + "reqd": 1 }, { "fieldname": "planned_end_time", "fieldtype": "Datetime", "label": "Planned End Time", "permlevel": 0, + "precision": "", + "reqd": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Actual Time and Cost", + "permlevel": 0, "precision": "" }, + { + "description": "in Minutes\nUpdated via 'Time Log'", + "fieldname": "actual_operation_time", + "fieldtype": "Float", + "label": "Actual Operation Time", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "description": "Hour Rate * Actual Operating Cost", + "fieldname": "actual_operating_cost", + "fieldtype": "Float", + "label": "Actual Operating Cost", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, { "fieldname": "column_break_11", "fieldtype": "Column Break", @@ -252,17 +276,21 @@ "fieldtype": "Datetime", "label": "Actual Start Time", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { + "description": "Updated via 'Time Log'", "fieldname": "actual_end_time", "fieldtype": "Datetime", "label": "Actual End Time", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus==1", "fieldname": "make_time_log", "fieldtype": "Button", "label": "Make Time Log", @@ -277,7 +305,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2014-11-25 13:34:10.697445", + "modified": "2014-12-01 14:06:40.068700", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order Operation", diff --git a/erpnext/manufacturing/doctype/workstation/test_records.json b/erpnext/manufacturing/doctype/workstation/test_records.json index c9ee893e8a..685c84e2d7 100644 --- a/erpnext/manufacturing/doctype/workstation/test_records.json +++ b/erpnext/manufacturing/doctype/workstation/test_records.json @@ -6,6 +6,7 @@ "warehouse": "_Test warehouse - _TC", "fixed_cycle_cost": 1000, "hour_rate":100, + "holiday_list": "_Test Holiday List", "workstation_operation_hours": [ { "start_time": "10:00:00", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index 6271a163cb..d3c7b56e8a 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -5,7 +5,15 @@ //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { - + frappe.call({ + type:"GET", + method:"erpnext.manufacturing.doctype.workstation.workstation.get_default_holiday_list", + callback: function(r) { + if(!r.exe && r.message){ + cur_frm.set_value("holiday_list", r.message); + } + } + }) } cur_frm.cscript.refresh = function(doc, cdt, cdn) { diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 45b16af85b..bde2a41187 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -150,6 +150,7 @@ "precision": "" }, { + "default": "", "fieldname": "holiday_list", "fieldtype": "Link", "label": "Holiday List", @@ -160,7 +161,7 @@ ], "icon": "icon-wrench", "idx": 1, - "modified": "2014-11-07 11:39:37.720913", + "modified": "2014-11-27 19:04:58.125107", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 52d644ca2c..6f864b49bd 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -5,10 +5,13 @@ from __future__ import unicode_literals import frappe import datetime from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, cint from frappe.model.document import Document +class WorkstationHolidayError(frappe.ValidationError): pass +class WorkstationIsClosedError(frappe.ValidationError): pass + class Workstation(Document): def update_bom_operation(self): bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` @@ -26,19 +29,26 @@ class Workstation(Document): def check_if_within_operating_hours(self, from_time, to_time): if self.check_workstation_for_operation_time(from_time, to_time): - frappe.msgprint(_("Warning: Time Log timings outside workstation Operating Hours !")) + frappe.throw(_("Time Log timings outside workstation Operating Hours !"), WorkstationIsClosedError) - msg = self.check_workstation_for_holiday(from_time, to_time) - if msg != None: - frappe.msgprint(msg) + if frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") == "No": + msg = self.check_workstation_for_holiday(from_time, to_time) + if msg != None: + frappe.throw(msg, WorkstationHolidayError) def check_workstation_for_operation_time(self, from_time, to_time): start_time = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') end_time = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') + max_time_diff = frappe.db.get_value("Manufacturing Settings", "None", "max_overtime") - if frappe.db.sql("""select start_time, end_time from `tabWorkstation Operation Hours` - where parent = %s and (%s end_time )""",(self.workstation_name, start_time, end_time), as_dict=1): - return 1 + for d in frappe.db.sql("""select time_to_sec(timediff( start_time, %s))/60 as st_diff , + time_to_sec(timediff( %s, end_time))/60 as et_diff from `tabWorkstation Operation Hours` + where parent = %s and (%s end_time )""", + (start_time, end_time, self.workstation_name, start_time, end_time), as_dict=1): + if cint(d.st_diff) > cint(max_time_diff): + return 1 + if cint(d.et_diff) > cint(max_time_diff): + return 1 def check_workstation_for_holiday(self, from_time, to_time): holiday_list = frappe.db.get_value("Workstation", self.workstation_name, "holiday_list") @@ -50,8 +60,11 @@ class Workstation(Document): %s and %s """,(holiday_list, start_date, end_date), as_dict=1): flag = 1 msg = msg + "\n" + d.holiday_date - if flag ==1: return msg else: - return None \ No newline at end of file + return None + +@frappe.whitelist() +def get_default_holiday_list(): + return frappe.db.get_value("Company", frappe.defaults.get_user_default("company"), "default_holiday_list") \ No newline at end of file diff --git a/erpnext/projects/doctype/time_log/test_time_log.py b/erpnext/projects/doctype/time_log/test_time_log.py index bc0a9dc24b..bfc4c05eab 100644 --- a/erpnext/projects/doctype/time_log/test_time_log.py +++ b/erpnext/projects/doctype/time_log/test_time_log.py @@ -1,10 +1,16 @@ # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from __future__ import unicode_literals import frappe import unittest from erpnext.projects.doctype.time_log.time_log import OverlapError +from erpnext.projects.doctype.time_log.time_log import NotSubmittedError + +from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError +from erpnext.manufacturing.doctype.workstation.workstation import WorkstationIsClosedError + from erpnext.projects.doctype.time_log_batch.test_time_log_batch import * class TestTimeLog(unittest.TestCase): @@ -17,5 +23,59 @@ class TestTimeLog(unittest.TestCase): frappe.db.sql("delete from `tabTime Log`") + def test_production_order_status(self): + prod_order = make_prod_order(self) + + prod_order.save() + + time_log = frappe.get_doc({ + "doctype": "Time Log", + "time_log_for": "Manufacturing", + "production_order": prod_order.name, + "qty": 1, + "from_time": "2014-12-26 00:00:00", + "to_time": "2014-12-26 00:00:00" + }) + + self.assertRaises(NotSubmittedError, time_log.save) + + def test_time_log_on_holiday(self): + prod_order = make_prod_order(self) + + prod_order.save() + prod_order.submit() + + time_log = frappe.get_doc({ + "doctype": "Time Log", + "time_log_for": "Manufacturing", + "production_order": prod_order.name, + "qty": 1, + "from_time": "2013-02-01 10:00:00", + "to_time": "2013-02-01 20:00:00", + "workstation": "_Test Workstation 1" + }) + self.assertRaises(WorkstationHolidayError , time_log.save) + + time_log.update({ + "from_time": "2013-02-02 09:00:00", + "to_time": "2013-02-02 20:00:00" + }) + self.assertRaises(WorkstationIsClosedError , time_log.save) + + time_log.from_time= "2013-02-02 09:30:00" + time_log.save() + time_log.submit() + time_log.cancel() + +def make_prod_order(self): + return frappe.get_doc({ + "doctype":"Production Order", + "production_item": "_Test FG Item 2", + "bom_no": "BOM/_Test FG Item 2/002", + "qty": 1, + "wip_warehouse": "_Test Warehouse - _TC", + "fg_warehouse": "_Test Warehouse 1 - _TC" + }) + test_records = frappe.get_test_records('Time Log') test_ignore = ["Time Log Batch", "Sales Invoice"] diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index ffbd8f315d..650996bb34 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -9,8 +9,9 @@ from frappe import _ from frappe.utils import cstr, cint, comma_and - class OverlapError(frappe.ValidationError): pass +class OverProductionError(frappe.ValidationError): pass +class NotSubmittedError(frappe.ValidationError): pass from frappe.model.document import Document @@ -19,9 +20,11 @@ class TimeLog(Document): def validate(self): self.set_status() self.validate_overlap() + self.validate_timings() self.calculate_total_hours() self.check_workstation_timings() self.validate_qty() + self.validate_production_order() def on_submit(self): self.update_production_order() @@ -47,6 +50,7 @@ class TimeLog(Document): self.status="Billed" def validate_overlap(self): + """Checks if 'Time Log' entries overlap each other. """ existing = frappe.db.sql_list("""select name from `tabTime Log` where owner=%s and ( (from_time between %s and %s) or @@ -61,6 +65,10 @@ class TimeLog(Document): if existing: frappe.throw(_("This Time Log conflicts with {0}").format(comma_and(existing)), OverlapError) + + def validate_timings(self): + if self.to_time < self.from_time: + frappe.throw(_("From Time cannot be greater than To Time")) def before_cancel(self): self.set_status() @@ -69,6 +77,7 @@ class TimeLog(Document): self.set_status() def update_production_order(self): + """Updates `start_date`, `end_date` for operation in Production Order.""" if self.time_log_for=="Manufacturing" and self.operation: d = self.get_qty_and_status() required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) @@ -84,6 +93,7 @@ class TimeLog(Document): self.production_order_update(dates, d.get('qty'), d['status']) def update_production_order_on_cancel(self): + """Updates operations in 'Production Order' when an associated 'Time Log' is cancelled.""" if self.time_log_for=="Manufacturing" and self.operation: d = frappe._dict() d = self.get_qty_and_status() @@ -91,6 +101,7 @@ class TimeLog(Document): self.production_order_update(dates, d.get('qty'), d.get('status')) def get_qty_and_status(self): + """Returns quantity and status of Operation in 'Time Log'. """ status = "Work in Progress" qty = cint(frappe.db.sql("""select sum(qty) as qty from `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", (self.production_order, self.operation),as_dict=1)[0].qty) @@ -102,30 +113,67 @@ class TimeLog(Document): } def get_production_dates(self): + """Returns Min From and Max To Dates of Time Logs against a specific Operation. """ return frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date from `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", (self.production_order, self.operation), as_dict=1)[0] def production_order_update(self, dates, qty, status): + """Updates 'Produuction Order' and sets 'Actual Start Time', 'Actual End Time', 'Status', 'Compleated Qty'. """ d = self.operation.split('. ',1) - frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s, - qty_completed = %s, status = %s where idx=%s and parent=%s and operation = %s """, - (dates.start_date, dates.end_date, qty, status, d[0], self.production_order, d[1] )) + actual_op_time = self.get_actual_op_time().time_diff + if actual_op_time == None: + actual_op_time = 0 + actual_op_cost = self.get_actual_op_cost(actual_op_time) + frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s, qty_completed = %s, + status = %s, actual_operation_time = %s, actual_operating_cost = %s where idx=%s and parent=%s and operation = %s """, + (dates.start_date, dates.end_date, qty, status, actual_op_time, actual_op_cost, d[0], self.production_order, d[1] )) + + def get_actual_op_time(self): + """Returns 'Actual Operating Time'. """ + return frappe.db.sql("""select sum(time_to_sec(timediff(to_time, from_time))/60) as time_diff from + `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", + (self.production_order, self.operation), as_dict = 1)[0] + + def get_actual_op_cost(self, actual_op_time): + """Returns 'Actual Operating Cost'. """ + if self.operation: + d = self.operation.split('. ',1) + idx = d[0] + operation = d[1] + hour_rate = frappe.db.sql("""select hour_rate from `tabProduction Order Operation` where idx=%s and + parent=%s and operation = %s""", (idx, self.production_order, operation), as_dict=1)[0].hour_rate + return hour_rate * actual_op_time + def check_workstation_timings(self): + """Checks if **Time Log** is between operating hours of the **Workstation**.""" if self.workstation: frappe.get_doc("Workstation", self.workstation).check_if_within_operating_hours(self.from_time, self.to_time) def validate_qty(self): + """Throws `OverProductionError` if quantity surpasses **Production Order** quantity.""" if self.qty == None: self.qty=0 required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) completed_qty = self.get_qty_and_status().get('qty') if (completed_qty + cint(self.qty)) > required_qty: - frappe.throw(_("Quantity cannot be greater than pending quantity that is {0}").format(required_qty)) - + frappe.throw(_("Quantity cannot be greater than pending quantity that is {0}").format(required_qty), OverProductionError) + + def validate_production_order(self): + """Throws 'NotSubmittedError' if **production order** is not submitted. """ + if self.production_order: + if frappe.db.get_value("Production Order", self.production_order, "docstatus") != 1 : + frappe.throw(_("You cannot make a time log against a production order that has not been submitted.") + , NotSubmittedError) + @frappe.whitelist() def get_workstation(production_order, operation): + """Returns workstation name from Production Order against an associated Operation. + + :param production_order string + :param operation string + """ if operation: d = operation.split('. ',1) idx = d[0] @@ -136,6 +184,12 @@ def get_workstation(production_order, operation): @frappe.whitelist() def get_events(start, end, filters=None): + """Returns events for Gantt / Calendar view rendering. + + :param start: Start date-time. + :param end: End date-time. + :param filters: Filters like workstation, project etc. + """ from frappe.desk.reportview import build_match_conditions if not frappe.has_permission("Time Log"): frappe.msgprint(_("No Permission"), raise_exception=1) diff --git a/erpnext/projects/doctype/time_log/time_log_list.html b/erpnext/projects/doctype/time_log/time_log_list.html index ee0b96f28c..96b8925edb 100644 --- a/erpnext/projects/doctype/time_log/time_log_list.html +++ b/erpnext/projects/doctype/time_log/time_log_list.html @@ -9,17 +9,31 @@ {% } %} + + {% if(doc.time_log_for == 'Manufacturing') { %} + + + + {% } %} + + {% if(doc.activity_type) { %} {%= doc.activity_type %} - - ({%= doc.hours + " " + __("hours") %}) - + {% } %} + {% if(doc.project) { %} {%= doc.project %} {% } %} + + + ({%= doc.hours + " " + __("hours") %}) + + diff --git a/erpnext/projects/doctype/time_log/time_log_list.js b/erpnext/projects/doctype/time_log/time_log_list.js index 664117484d..6115607ade 100644 --- a/erpnext/projects/doctype/time_log/time_log_list.js +++ b/erpnext/projects/doctype/time_log/time_log_list.js @@ -3,7 +3,7 @@ // render frappe.listview_settings['Time Log'] = { - add_fields: ["status", "billable", "activity_type", "task", "project", "hours"], + add_fields: ["status", "billable", "activity_type", "task", "project", "hours", "time_log_for"], selectable: true, onload: function(me) { me.appframe.add_primary_action(__("Make Time Log Batch"), function() { diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 3439f0a9b1..05e49ba567 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -160,6 +160,14 @@ "options": "Account", "permlevel": 0 }, + { + "fieldname": "default_holiday_list", + "fieldtype": "Link", + "label": "Default Holiday List", + "options": "Holiday List", + "permlevel": 0, + "precision": "" + }, { "fieldname": "column_break0", "fieldtype": "Column Break", @@ -356,7 +364,7 @@ ], "icon": "icon-building", "idx": 1, - "modified": "2014-08-29 15:50:18.539228", + "modified": "2014-11-27 18:15:48.909416", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/templates/form_grid/production_order_grid.html b/erpnext/templates/form_grid/production_order_grid.html new file mode 100644 index 0000000000..080f80f001 --- /dev/null +++ b/erpnext/templates/form_grid/production_order_grid.html @@ -0,0 +1,34 @@ +{% var visible_columns = row.get_visible_columns(["operation", + "opn_description", "status", "qty_completed", "workstation"]); +%} + +{% if(!doc) { %} +
+
{%= __("Operation") %}
+
{%= __("Workstation") %}
+
{%= __("Completed Qty") %}
+
+{% } else { %} +
+
+ {%= doc.operation %} + + {%= doc.status %} + + {% include "templates/form_grid/includes/visible_cols.html" %} +
+ {%= doc.get_formatted("opn_description") %} +
+
+ + +
+ {%= doc.get_formatted("workstation") %} +
+ + +
+ {%= doc.get_formatted("qty_completed") %} +
+
+{% } %} diff --git a/erpnext/templates/form_grid/stock_entry_grid.html b/erpnext/templates/form_grid/stock_entry_grid.html index c5f3ecd899..9f913087c5 100644 --- a/erpnext/templates/form_grid/stock_entry_grid.html +++ b/erpnext/templates/form_grid/stock_entry_grid.html @@ -40,7 +40,8 @@
{%= doc.get_formatted("amount") %}
- {%= doc.get_formatted("incoming_rate") %}
+ {%= doc.get_formatted("incoming_rate") %} +
{% } %} From 9712e7dad41f4bd06f422283fb6ed0e06ab5bb90 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Tue, 9 Dec 2014 17:11:26 +0530 Subject: [PATCH 05/10] holiday logic removed --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index f275d7d2da..03d9d03724 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -411,4 +411,3 @@ cur_frm.set_query("debit_to", function(doc) { ] } }); - From 41781ce6869a8d82a2834ed1df440adb643ed993 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Tue, 16 Dec 2014 14:22:30 +0530 Subject: [PATCH 06/10] test case fix - holiday list --- erpnext/hr/doctype/holiday_list/test_records.json | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/hr/doctype/holiday_list/test_records.json b/erpnext/hr/doctype/holiday_list/test_records.json index 34a4894947..be7b178112 100644 --- a/erpnext/hr/doctype/holiday_list/test_records.json +++ b/erpnext/hr/doctype/holiday_list/test_records.json @@ -1,7 +1,6 @@ [ { "doctype": "Holiday List", - "name": "_Test Holiday List 1", "fiscal_year": "_Test Fiscal Year 2013", "holiday_list_details": [ { From 1fa7171bfaeff1c2ff4c823334640d0c39379ec0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 17 Dec 2014 16:22:02 +0530 Subject: [PATCH 07/10] minor fixes --- erpnext/manufacturing/doctype/bom/bom.js | 72 ++++++++++--------- .../doctype/bom_operation/bom_operation.json | 41 ++++++----- .../production_order/production_order.js | 10 +-- .../production_order/production_order.json | 58 ++++++++++++--- .../production_order_operation.json | 18 ++--- .../form_grid/production_order_grid.html | 34 --------- 6 files changed, 124 insertions(+), 109 deletions(-) delete mode 100644 erpnext/templates/form_grid/production_order_grid.html diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 5c099c43e3..ce9ac0e35d 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -31,7 +31,7 @@ cur_frm.cscript.with_operations = function(doc) { } erpnext.bom.set_operation = function(doc) { - var op_table = doc.bom_operations || []; + var op_table = doc["bom_operations"] || []; var operations = []; for (var i=0, j=op_table.length; i -
{%= __("Operation") %}
-
{%= __("Workstation") %}
-
{%= __("Completed Qty") %}
- -{% } else { %} -
-
- {%= doc.operation %} - - {%= doc.status %} - - {% include "templates/form_grid/includes/visible_cols.html" %} -
- {%= doc.get_formatted("opn_description") %} -
-
- - -
- {%= doc.get_formatted("workstation") %} -
- - -
- {%= doc.get_formatted("qty_completed") %} -
-
-{% } %} From 0f4350dd613cce2855c76aa2c944aa79b8d0456f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 19 Dec 2014 10:57:46 +0530 Subject: [PATCH 08/10] capacity planning fixes and cleanup --- erpnext/manufacturing/doctype/bom/bom.js | 28 ++- erpnext/manufacturing/doctype/bom/bom.json | 64 ++++--- erpnext/manufacturing/doctype/bom/bom.py | 20 ++- .../doctype/bom_operation/bom_operation.json | 162 ++++++++--------- .../manufacturing_settings.json | 4 +- .../doctype/operation/operation.json | 23 ++- .../production_order/production_order.js | 14 +- .../production_order/production_order.json | 79 +++++---- .../production_order/production_order.py | 92 ++++++---- .../production_order_calendar.js | 8 +- .../production_order_operation.json | 45 +++-- .../doctype/workstation/workstation.json | 28 +-- .../doctype/workstation/workstation.py | 88 ++++----- erpnext/patches.txt | 1 + erpnext/patches/v5_0/capacity_planning.py | 23 +++ erpnext/projects/doctype/time_log/time_log.js | 19 +- .../projects/doctype/time_log/time_log.json | 23 +-- erpnext/projects/doctype/time_log/time_log.py | 167 +++++++----------- 18 files changed, 464 insertions(+), 424 deletions(-) create mode 100644 erpnext/patches/v5_0/capacity_planning.py diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index ce9ac0e35d..1fef504592 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -114,23 +114,16 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) { erpnext.bom.calculate_op_cost = function(doc) { var op = doc.bom_operations || []; - total_op_cost = 0; + doc.total_variable_cost, doc.total_fixed_cost = 0.0, 0.0; for(var i=0;i end_time )""", - (start_time, end_time, self.workstation_name, start_time, end_time), as_dict=1): - if cint(d.st_diff) > cint(max_time_diff): - return 1 - if cint(d.et_diff) > cint(max_time_diff): - return 1 - - def check_workstation_for_holiday(self, from_time, to_time): - holiday_list = frappe.db.get_value("Workstation", self.workstation_name, "holiday_list") - start_date = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') - end_date = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') - msg = _("Workstation is closed on the following dates as per Holiday List:") - flag = 0 - for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s and holiday_date between - %s and %s """,(holiday_list, start_date, end_date), as_dict=1): - flag = 1 - msg = msg + "\n" + d.holiday_date - if flag ==1: - return msg - else: - return None @frappe.whitelist() def get_default_holiday_list(): - return frappe.db.get_value("Company", frappe.defaults.get_user_default("company"), "default_holiday_list") \ No newline at end of file + return frappe.db.get_value("Company", frappe.defaults.get_user_default("company"), "default_holiday_list") + +def check_if_within_operating_hours(workstation, from_time, to_time): + if check_workstation_for_operation_time(workstation, from_time, to_time): + frappe.throw(_("Time Log timings outside workstation Operating Hours !"), WorkstationIsClosedError) + + if frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") == "No": + msg = check_workstation_for_holiday(workstation, from_time, to_time) + if msg != None: + frappe.throw(msg, WorkstationHolidayError) + +def check_workstation_for_operation_time(workstation, from_time, to_time): + start_time = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') + end_time = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') + max_time_diff = frappe.db.get_value("Manufacturing Settings", "None", "max_overtime") + + for d in frappe.db.sql("""select time_to_sec(timediff( start_time, %s))/60 as st_diff , + time_to_sec(timediff( %s, end_time))/60 as et_diff from `tabWorkstation Operation Hours` + where parent = %s and (%s end_time )""", + (start_time, end_time, workstation, start_time, end_time), as_dict=1): + if cint(d.st_diff) > cint(max_time_diff): + return 1 + if cint(d.et_diff) > cint(max_time_diff): + return 1 + +def check_workstation_for_holiday(workstation, from_time, to_time): + holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") + start_date = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') + end_date = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') + msg = _("Workstation is closed on the following dates as per Holiday List:") + flag = 0 + for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s and holiday_date between + %s and %s """,(holiday_list, start_date, end_date), as_dict=1): + flag = 1 + msg = msg + "\n" + d.holiday_date + if flag ==1: + return msg + else: + return None diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a81527ec79..07cfdbf040 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -98,3 +98,4 @@ execute:frappe.reload_doc('stock', 'doctype', 'item') execute:frappe.db.sql("update `tabItem` i set apply_warehouse_wise_reorder_level=1, re_order_level=0, re_order_qty=0 where exists(select name from `tabItem Reorder` where parent=i.name)") execute:frappe.rename_doc("DocType", "Support Ticket", "Issue", force=True) erpnext.patches.v5_0.set_default_company_in_bom +erpnext.patches.v5_0.capacity_planning diff --git a/erpnext/patches/v5_0/capacity_planning.py b/erpnext/patches/v5_0/capacity_planning.py new file mode 100644 index 0000000000..ce964353b0 --- /dev/null +++ b/erpnext/patches/v5_0/capacity_planning.py @@ -0,0 +1,23 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + +def execute(): + for dt in ["workstation", "bom", "bom_operation"]: + frappe.reload_doc("manufacturing", "doctype", dt) + + frappe.db.sql("update `tabWorkstation` set fixed_cost = fixed_cycle_cost, total_variable_cost = overhead") + + frappe.db.sql("update `tabBOM Operation` set fixed_cost = fixed_cycle_cost") + + for d in frappe.db.sql("select name from `tabBOM` where docstatus < 2"): + try: + bom = frappe.get_doc('BOM', d[0]) + if bom.docstatus == 1: + bom.ignore_validate_update_after_submit = True + bom.calculate_cost() + bom.save() + except: + print "error", frappe.get_traceback() + pass diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index 36ca2d2ba8..2c74bb328e 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -26,10 +26,18 @@ frappe.ui.form.on("Time Log", "to_time", function(frm) { "hours")); }); +cur_frm.set_query("production_order", function(doc) { + return { + "filters": { + "docstatus": 1 + } + }; +}); + cur_frm.add_fetch('task','project','project'); $.extend(cur_frm.cscript, { - production_order: function(doc) { + production_order: function(doc) { if (doc.production_order){ var operations = []; frappe.model.with_doc("Production Order", doc.production_order, function(pro) { @@ -44,13 +52,16 @@ $.extend(cur_frm.cscript, { }, operation: function(doc) { - return cur_frm.call({ + return frappe.call({ method: "erpnext.projects.doctype.time_log.time_log.get_workstation", - args: { + args: { "production_order": doc.production_order, "operation": doc.operation }, callback: function(r) { + if(!r.exc) { + cur_frm.set_value("workstation", r.message) + } doc.workstation = r.workstation; } }); @@ -59,4 +70,4 @@ $.extend(cur_frm.cscript, { if (cur_frm.doc.time_log_for == "Manufacturing") { cur_frm.cscript.onload = cur_frm.cscript.production_order; -} \ No newline at end of file +} diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index 6e2706de2c..4afc0a0469 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -20,10 +20,10 @@ "fieldname": "time_log_for", "fieldtype": "Select", "label": "Time Log For", - "options": "Project\nManufacturing", + "options": "\nProject\nManufacturing", "permlevel": 0, "precision": "", - "reqd": 1 + "reqd": 0 }, { "fieldname": "from_time", @@ -68,7 +68,7 @@ "reqd": 0 }, { - "depends_on": "eval:doc.time_log_for == 'Project'", + "depends_on": "eval:doc.time_log_for != 'Manufacturing'", "fieldname": "activity_type", "fieldtype": "Link", "in_list_view": 1, @@ -79,7 +79,7 @@ "reqd": 0 }, { - "depends_on": "eval:doc.time_log_for == 'Project'", + "depends_on": "eval:doc.time_log_for != 'Manufacturing'", "fieldname": "task", "fieldtype": "Link", "label": "Task", @@ -112,13 +112,15 @@ "label": "Workstation", "options": "Workstation", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { - "depends_on": "eval:doc.time_log_for == 'Manufacturing'", - "fieldname": "qty", - "fieldtype": "Float", - "label": "Quantity", + "default": "Work in Progress", + "fieldname": "operation_status", + "fieldtype": "Select", + "label": "Operation Status", + "options": "\nWork in Progress\nCompleted", "permlevel": 0, "precision": "" }, @@ -150,6 +152,7 @@ "read_only": 0 }, { + "depends_on": "eval:doc.time_log_for == 'Project'", "fieldname": "project", "fieldtype": "Link", "in_list_view": 1, @@ -197,7 +200,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2014-11-19 11:39:02.633802", + "modified": "2014-12-18 17:21:01.520646", "modified_by": "Administrator", "module": "Projects", "name": "Time Log", diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index 650996bb34..db08abc852 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -from frappe.utils import cstr, cint, comma_and +from frappe.utils import cstr, comma_and, flt class OverlapError(frappe.ValidationError): pass @@ -16,25 +16,25 @@ class NotSubmittedError(frappe.ValidationError): pass from frappe.model.document import Document class TimeLog(Document): - def validate(self): self.set_status() self.validate_overlap() self.validate_timings() self.calculate_total_hours() self.check_workstation_timings() - self.validate_qty() self.validate_production_order() def on_submit(self): self.update_production_order() def on_cancel(self): - self.update_production_order_on_cancel() + self.update_production_order() - def calculate_total_hours(self): - from frappe.utils import time_diff_in_hours - self.hours = time_diff_in_hours(self.to_time, self.from_time) + def before_update_after_submit(self): + self.set_status() + + def before_cancel(self): + self.set_status() def set_status(self): self.status = { @@ -65,127 +65,84 @@ class TimeLog(Document): if existing: frappe.throw(_("This Time Log conflicts with {0}").format(comma_and(existing)), OverlapError) - + def validate_timings(self): if self.to_time < self.from_time: frappe.throw(_("From Time cannot be greater than To Time")) - def before_cancel(self): - self.set_status() + def calculate_total_hours(self): + from frappe.utils import time_diff_in_seconds + self.hours = flt(time_diff_in_seconds(self.to_time, self.from_time)) / 3600 - def before_update_after_submit(self): - self.set_status() - - def update_production_order(self): - """Updates `start_date`, `end_date` for operation in Production Order.""" - if self.time_log_for=="Manufacturing" and self.operation: - d = self.get_qty_and_status() - required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) - if d.get('qty') == required_qty: - d['status'] = "Completed" - - dates = self.get_production_dates() - if self.from_time < dates.start_date: - dates.start_date = self.from_time - if self.to_time > dates.end_date: - dates.end_date = self.to_time - - self.production_order_update(dates, d.get('qty'), d['status']) - - def update_production_order_on_cancel(self): - """Updates operations in 'Production Order' when an associated 'Time Log' is cancelled.""" - if self.time_log_for=="Manufacturing" and self.operation: - d = frappe._dict() - d = self.get_qty_and_status() - dates = self.get_production_dates() - self.production_order_update(dates, d.get('qty'), d.get('status')) - - def get_qty_and_status(self): - """Returns quantity and status of Operation in 'Time Log'. """ - status = "Work in Progress" - qty = cint(frappe.db.sql("""select sum(qty) as qty from `tabTime Log` where production_order = %s - and operation = %s and docstatus=1""", (self.production_order, self.operation),as_dict=1)[0].qty) - if qty == 0: - status = "Pending" - return { - "qty": qty, - "status": status - } - - def get_production_dates(self): - """Returns Min From and Max To Dates of Time Logs against a specific Operation. """ - return frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date from `tabTime Log` - where production_order = %s and operation = %s and docstatus=1""", - (self.production_order, self.operation), as_dict=1)[0] - - def production_order_update(self, dates, qty, status): - """Updates 'Produuction Order' and sets 'Actual Start Time', 'Actual End Time', 'Status', 'Compleated Qty'. """ - d = self.operation.split('. ',1) - actual_op_time = self.get_actual_op_time().time_diff - if actual_op_time == None: - actual_op_time = 0 - actual_op_cost = self.get_actual_op_cost(actual_op_time) - frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s, qty_completed = %s, - status = %s, actual_operation_time = %s, actual_operating_cost = %s where idx=%s and parent=%s and operation = %s """, - (dates.start_date, dates.end_date, qty, status, actual_op_time, actual_op_cost, d[0], self.production_order, d[1] )) - - def get_actual_op_time(self): - """Returns 'Actual Operating Time'. """ - return frappe.db.sql("""select sum(time_to_sec(timediff(to_time, from_time))/60) as time_diff from - `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", - (self.production_order, self.operation), as_dict = 1)[0] - - def get_actual_op_cost(self, actual_op_time): - """Returns 'Actual Operating Cost'. """ - if self.operation: - d = self.operation.split('. ',1) - idx = d[0] - operation = d[1] - - hour_rate = frappe.db.sql("""select hour_rate from `tabProduction Order Operation` where idx=%s and - parent=%s and operation = %s""", (idx, self.production_order, operation), as_dict=1)[0].hour_rate - return hour_rate * actual_op_time - def check_workstation_timings(self): """Checks if **Time Log** is between operating hours of the **Workstation**.""" if self.workstation: - frappe.get_doc("Workstation", self.workstation).check_if_within_operating_hours(self.from_time, self.to_time) + from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours + check_if_within_operating_hours(self.workstation, self.from_time, self.to_time) - def validate_qty(self): - """Throws `OverProductionError` if quantity surpasses **Production Order** quantity.""" - if self.qty == None: - self.qty=0 - required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) - completed_qty = self.get_qty_and_status().get('qty') - if (completed_qty + cint(self.qty)) > required_qty: - frappe.throw(_("Quantity cannot be greater than pending quantity that is {0}").format(required_qty), OverProductionError) - def validate_production_order(self): """Throws 'NotSubmittedError' if **production order** is not submitted. """ + if self.production_order: if frappe.db.get_value("Production Order", self.production_order, "docstatus") != 1 : - frappe.throw(_("You cannot make a time log against a production order that has not been submitted.") - , NotSubmittedError) - + frappe.throw(_("You can make a time log only against a submitted production order"), NotSubmittedError) + + def update_production_order(self): + """Updates `start_date`, `end_date`, `status` for operation in Production Order.""" + + if self.time_log_for=="Manufacturing" and self.operation: + dates = self.get_operation_start_end_time() + op_status = self.get_op_status() + actual_op_time = self.get_actual_op_time() + + d = self.operation.split('. ',1) + + frappe.db.sql("""update `tabProduction Order Operation` + set actual_start_time = %s, actual_end_time = %s, status = %s, actual_operation_time = %s + where parent=%s and idx=%s and operation = %s""", + (dates.start_date, dates.end_date, op_status, actual_op_time, + self.production_order, d[0], d[1])) + + frappe.get_doc("Production Order", self.production_order).save() + + def get_operation_start_end_time(self): + """Returns Min From and Max To Dates of Time Logs against a specific Operation. """ + return frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date from `tabTime Log` + where production_order = %s and operation = %s and docstatus=1""", + (self.production_order, self.operation), as_dict=1)[0] + + def get_actual_op_time(self): + """Returns 'Actual Operating Time'. """ + actual_time = frappe.db.sql("""select sum(hours*60) as time_diff from + `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", + (self.production_order, self.operation)) + return actual_time[0][0] if actual_time else 0 + + def get_op_status(self): + status = frappe.db.sql("""select operation_status from `tabTime Log` + where production_order=%s and operation=%s and docstatus=1 + order by to_time desc limit 1""", (self.production_order, self.operation)) + + return status if status else self.status + @frappe.whitelist() def get_workstation(production_order, operation): """Returns workstation name from Production Order against an associated Operation. - + :param production_order string :param operation string """ if operation: - d = operation.split('. ',1) - idx = d[0] - operation = d[1] + idx, operation = operation.split('. ',1) - return frappe.db.sql("""select workstation from `tabProduction Order Operation` where idx=%s and - parent=%s and operation = %s""", (idx, production_order, operation), as_dict=1)[0] + workstation = frappe.db.sql("""select workstation from `tabProduction Order Operation` where idx=%s and + parent=%s and operation = %s""", (idx, production_order, operation)) + return workstation[0][0] if workstation else "" @frappe.whitelist() def get_events(start, end, filters=None): """Returns events for Gantt / Calendar view rendering. - + :param start: Start date-time. :param end: End date-time. :param filters: Filters like workstation, project etc. @@ -194,8 +151,6 @@ def get_events(start, end, filters=None): if not frappe.has_permission("Time Log"): frappe.msgprint(_("No Permission"), raise_exception=1) - match = build_match_conditions("Time Log") - conditions = build_match_conditions("Time Log") conditions = conditions and (" and " + conditions) or "" if filters: @@ -211,7 +166,7 @@ def get_events(start, end, filters=None): "start": start, "end": end }, as_dict=True, update={"allDay": 0}) - + for d in data: d.title = d.name + ": " + (d.activity_type or d.production_order or "") if d.task: From 0de6256bccf8e6f8cf5a076c58f5442c143ed2ea Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Dec 2014 14:31:53 +0530 Subject: [PATCH 09/10] time log and workstations --- .../manufacturing_settings.json | 40 +++++----- .../production_order/production_order.json | 30 ++++++-- .../production_order/production_order.py | 12 +-- .../production_order/test_production_order.py | 20 ++--- .../production_order_operation.json | 4 +- .../doctype/workstation/workstation.json | 4 +- .../doctype/workstation/workstation.py | 76 ++++++++++--------- .../__init__.py | 0 .../workstation_operation_hour.json} | 63 +++++++++++++-- .../workstation_operation_hour.py} | 2 +- erpnext/projects/doctype/time_log/time_log.js | 18 +++-- .../projects/doctype/time_log/time_log.json | 4 +- erpnext/projects/doctype/time_log/time_log.py | 39 +++++++--- 13 files changed, 203 insertions(+), 109 deletions(-) rename erpnext/manufacturing/doctype/{workstation_operation_hours => workstation_operation_hour}/__init__.py (100%) rename erpnext/manufacturing/doctype/{workstation_operation_hours/workstation_operation_hours.json => workstation_operation_hour/workstation_operation_hour.json} (53%) rename erpnext/manufacturing/doctype/{workstation_operation_hours/workstation_operation_hours.py => workstation_operation_hour/workstation_operation_hour.py} (85%) diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 23c223e7bc..a5bdecd8db 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -9,33 +9,26 @@ "document_type": "Master", "fields": [ { - "allow_on_submit": 0, - "default": "30", - "description": "Maximum Overtime allowed against an workstation.\n( in mins )", - "fieldname": "max_overtime", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Maximum Overtime", - "no_copy": 0, + "description": "Will not allow to make time logs outside \"Workstation operation timings\"", + "fieldname": "dont_allow_overtime", + "fieldtype": "Check", + "label": "Don't allow overtime", "permlevel": 0, - "precision": "", - "print_hide": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "precision": "" }, { - "default": "No", + "default": "", "fieldname": "allow_production_on_holidays", - "fieldtype": "Select", + "fieldtype": "Check", + "in_list_view": 1, "label": "Allow Production on Holidays", - "options": "Yes\nNo", + "options": "", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break", "permlevel": 0, "precision": "" }, @@ -44,6 +37,7 @@ "description": "Delay in start time of production order operations if automatically make time logs is used.\n(in mins)", "fieldname": "operations_start_delay", "fieldtype": "Float", + "in_list_view": 1, "label": "Operations Start Delay", "permlevel": 0, "precision": "" @@ -57,7 +51,7 @@ "is_submittable": 0, "issingle": 1, "istable": 0, - "modified": "2014-12-18 16:22:26.052642", + "modified": "2014-12-22 12:43:15.261503", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.json b/erpnext/manufacturing/doctype/production_order/production_order.json index f34e83c423..da2bd5bf2a 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -122,6 +122,20 @@ "permlevel": 0, "read_only": 0 }, + { + "fieldname": "planned_start_date", + "fieldtype": "Datetime", + "label": "Planned Start Date", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "planned_end_date", + "fieldtype": "Datetime", + "label": "Planned End Date", + "permlevel": 0, + "precision": "" + }, { "fieldname": "column_break_13", "fieldtype": "Column Break", @@ -129,18 +143,20 @@ "precision": "" }, { - "fieldname": "production_start_date", + "fieldname": "actual_start_date", "fieldtype": "Datetime", - "label": "Production Start Date", + "label": "Actual Start Date", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { - "fieldname": "production_end_date", + "fieldname": "actual_end_date", "fieldtype": "Datetime", - "label": "Production End Date", + "label": "Actual End Date", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "fieldname": "warehouses", @@ -331,7 +347,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2014-12-18 15:06:41.802390", + "modified": "2014-12-19 14:23:50.701164", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 5fb92d14a8..a075f74ad0 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -64,8 +64,8 @@ class ProductionOrder(Document): self.planned_variable_cost, self.actual_variable_cost = 0.0, 0.0 for d in self.get("production_order_operations"): - d.actual_variable_cost = flt(d.hour_rate) * flt(d.actual_operation_time) / 60 \ - if d.actual_operation_time else d.time_in_mins + + d.actual_variable_cost = flt(d.hour_rate) * flt(d.actual_operation_time) / 60 self.planned_variable_cost += flt(d.variable_cost) self.actual_variable_cost += flt(d.actual_variable_cost) @@ -254,14 +254,16 @@ def get_events(start, end, filters=None): if filters[key]: conditions += " and " + key + ' = "' + filters[key].replace('"', '\"') + '"' - data = frappe.db.sql("""select name,production_item, production_start_date, production_end_date from `tabProduction Order` + data = frappe.db.sql("""select name,production_item, production_start_date, production_end_date + from `tabProduction Order` where ((ifnull(production_start_date, '0000-00-00')!= '0000-00-00') \ and (production_start_date between %(start)s and %(end)s) \ or ((ifnull(production_start_date, '0000-00-00')!= '0000-00-00') \ - and production_end_date between %(start)s and %(end)s)){conditions}""".format(conditions=conditions), { + and production_end_date between %(start)s and %(end)s)) {conditions} + """.format(conditions=conditions), { "start": start, "end": end - }, as_dict=True, update={"allDay": 0}) + }, as_dict=True, update={"allDay": 0}) return data @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index 945e986a3f..49db178f45 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -74,19 +74,19 @@ class TestProductionOrder(unittest.TestCase): prod_order.production_order_operations[0].update({ "planned_start_time": "2014-11-25 00:00:00", "planned_end_time": "2014-11-25 10:00:00", - "hour_rate": 10 + "hour_rate": 10 }) prod_order.insert() - + d = prod_order.production_order_operations[0] from erpnext.manufacturing.doctype.production_order.production_order import make_time_log from frappe.utils import cstr from frappe.utils import time_diff_in_hours - + prod_order.submit() - + time_log = make_time_log( prod_order.name, cstr(d.idx) + ". " + d.operation, \ d.planned_start_time, d.planned_end_time, prod_order.qty - d.qty_completed) @@ -100,11 +100,11 @@ class TestProductionOrder(unittest.TestCase): manufacturing_settings = frappe.get_doc({ "doctype": "Manufacturing Settings", "maximum_overtime": 30, - "allow_production_on_holidays": "No" + "allow_production_on_holidays": 0 }) - + manufacturing_settings.save() - + prod_order.load_from_db() self.assertEqual(prod_order.production_order_operations[0].status, "Completed") self.assertEqual(prod_order.production_order_operations[0].qty_completed, prod_order.qty) @@ -114,13 +114,13 @@ class TestProductionOrder(unittest.TestCase): self.assertEqual(prod_order.production_order_operations[0].actual_operation_time, 600) self.assertEqual(prod_order.production_order_operations[0].actual_operating_cost, 6000) - + time_log.cancel() prod_order.load_from_db() self.assertEqual(prod_order.production_order_operations[0].status, "Pending") self.assertEqual(prod_order.production_order_operations[0].qty_completed, 0) - + self.assertEqual(prod_order.production_order_operations[0].actual_operation_time, 0) self.assertEqual(prod_order.production_order_operations[0].actual_operating_cost, 0) @@ -132,5 +132,5 @@ class TestProductionOrder(unittest.TestCase): "docstatus": 0 }) self.assertRaises(OverProductionError, time_log2.save) - + test_records = frappe.get_test_records('Production Order') diff --git a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json index 13b49c1ee8..f17a055970 100644 --- a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json @@ -277,7 +277,7 @@ }, { "allow_on_submit": 1, - "depends_on": "eval:doc.docstatus==1", + "depends_on": "eval:(doc.docstatus==1 && doc.status!=\"Completed\")", "fieldname": "make_time_log", "fieldtype": "Button", "label": "Make Time Log", @@ -292,7 +292,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2014-12-18 12:18:51.655535", + "modified": "2014-12-19 12:49:49.918120", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order Operation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 21cafc04fa..37932bc78c 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -154,14 +154,14 @@ "fieldname": "workstation_operation_hours", "fieldtype": "Table", "label": "Workstation Operation Hours", - "options": "Workstation Operation Hours", + "options": "Workstation Operation Hour", "permlevel": 0, "precision": "" } ], "icon": "icon-wrench", "idx": 1, - "modified": "2014-12-18 13:01:47.143326", + "modified": "2014-12-22 14:18:40.253034", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index a0b150c84f..b95ca45dd7 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -5,12 +5,13 @@ from __future__ import unicode_literals import frappe import datetime from frappe import _ -from frappe.utils import flt, cint +from frappe.utils import flt, cint, getdate, formatdate, comma_and from frappe.model.document import Document class WorkstationHolidayError(frappe.ValidationError): pass class WorkstationIsClosedError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): pass class Workstation(Document): def update_bom_operation(self): @@ -22,51 +23,58 @@ class Workstation(Document): (self.hour_rate, bom_no[0], self.name)) def on_update(self): + self.validate_overlap_for_operation_timings() + frappe.db.set(self, 'total_variable_cost', flt(self.hour_rate_electricity) + - flt(self.hour_rate_consumable) + flt(self.hour_rate_rent)) + flt(self.hour_rate_consumable) + flt(self.hour_rate_rent)) frappe.db.set(self, 'hour_rate', flt(self.hour_rate_labour) + flt(self.total_variable_cost)) + self.update_bom_operation() + def validate_overlap_for_operation_timings(self): + for d in self.get("workstation_operation_hours"): + existing = frappe.db.sql_list("""select idx from `tabWorkstation Operation Hours` + where parent = %s and name != %s + and ( + (start_time between %s and %s) or + (end_time between %s and %s) or + (%s between start_time and end_time)) + """, (self.name, d.name, d.start_time, d.end_time, d.start_time, d.end_time, d.start_time)) + if existing: + frappe.throw(_("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError) @frappe.whitelist() def get_default_holiday_list(): return frappe.db.get_value("Company", frappe.defaults.get_user_default("company"), "default_holiday_list") -def check_if_within_operating_hours(workstation, from_time, to_time): - if check_workstation_for_operation_time(workstation, from_time, to_time): - frappe.throw(_("Time Log timings outside workstation Operating Hours !"), WorkstationIsClosedError) +def check_if_within_operating_hours(workstation, from_datetime, to_datetime): + if not is_within_operating_hours(workstation, from_datetime, to_datetime): + frappe.throw(_("Time Log timings outside workstation operating hours"), WorkstationIsClosedError) - if frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") == "No": - msg = check_workstation_for_holiday(workstation, from_time, to_time) - if msg != None: - frappe.throw(msg, WorkstationHolidayError) + if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")): + check_workstation_for_holiday(workstation, from_datetime, to_datetime) -def check_workstation_for_operation_time(workstation, from_time, to_time): - start_time = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') - end_time = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') - max_time_diff = frappe.db.get_value("Manufacturing Settings", "None", "max_overtime") +def is_within_operating_hours(workstation, from_datetime, to_datetime): + if not cint(frappe.db.get_value("Manufacturing Settings", None, "dont_allow_overtime")): + return True - for d in frappe.db.sql("""select time_to_sec(timediff( start_time, %s))/60 as st_diff , - time_to_sec(timediff( %s, end_time))/60 as et_diff from `tabWorkstation Operation Hours` - where parent = %s and (%s end_time )""", - (start_time, end_time, workstation, start_time, end_time), as_dict=1): - if cint(d.st_diff) > cint(max_time_diff): - return 1 - if cint(d.et_diff) > cint(max_time_diff): - return 1 + start_time = datetime.datetime.strptime(from_datetime,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') + end_time = datetime.datetime.strptime(to_datetime,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') -def check_workstation_for_holiday(workstation, from_time, to_time): + for d in frappe.db.sql("""select start_time, end_time from `tabWorkstation Operation Hours` + where parent = %s and ifnull(enabled, 0) = 1""", workstation, as_dict=1): + if d.end_time >= start_time >= d.start_time and d.end_time >= end_time >= d.start_time: + return True + +def check_workstation_for_holiday(workstation, from_datetime, to_datetime): holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") - start_date = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') - end_date = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') - msg = _("Workstation is closed on the following dates as per Holiday List:") - flag = 0 - for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s and holiday_date between - %s and %s """,(holiday_list, start_date, end_date), as_dict=1): - flag = 1 - msg = msg + "\n" + d.holiday_date - if flag ==1: - return msg - else: - return None + if holiday_list: + applicable_holidays = [] + for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s + and holiday_date between %s and %s """, (holiday_list, getdate(from_datetime), getdate(to_datetime))): + applicable_holidays.append(formatdate(d[0])) + + if applicable_holidays: + frappe.throw(_("Workstation is closed on the following dates as per Holiday List: {0}") + .format(holiday_list) + "\n" + "\n".join(applicable_holidays), WorkstationHolidayError) diff --git a/erpnext/manufacturing/doctype/workstation_operation_hours/__init__.py b/erpnext/manufacturing/doctype/workstation_operation_hour/__init__.py similarity index 100% rename from erpnext/manufacturing/doctype/workstation_operation_hours/__init__.py rename to erpnext/manufacturing/doctype/workstation_operation_hour/__init__.py diff --git a/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.json b/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.json similarity index 53% rename from erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.json rename to erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.json index 8a064ca40f..ebfa0ef7c5 100644 --- a/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.json +++ b/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.json @@ -2,7 +2,7 @@ "allow_copy": 0, "allow_import": 0, "allow_rename": 0, - "creation": "2014-10-29 13:00:43.921508", + "creation": "2014-12-22 14:18:20.786493", "custom": 0, "docstatus": 0, "doctype": "DocType", @@ -29,10 +29,23 @@ "unique": 0 }, { - "fieldname": "section_break_2", + "allow_on_submit": 0, + "fieldname": "column_break_2", "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, "permlevel": 0, - "precision": "" + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { "allow_on_submit": 0, @@ -53,6 +66,46 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "section_break_2", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Enabled", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, @@ -62,10 +115,10 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2014-10-29 13:02:24.631554", + "modified": "2014-12-22 14:18:31.091653", "modified_by": "Administrator", "module": "Manufacturing", - "name": "Workstation Operation Hours", + "name": "Workstation Operation Hour", "name_case": "", "owner": "Administrator", "permissions": [], diff --git a/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.py b/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.py similarity index 85% rename from erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.py rename to erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.py index dfac1f8c42..1b8c51960e 100644 --- a/erpnext/manufacturing/doctype/workstation_operation_hours/workstation_operation_hours.py +++ b/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.py @@ -5,5 +5,5 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -class WorkstationOperationHours(Document): +class WorkstationOperationHour(Document): pass diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index 2c74bb328e..e255843872 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -5,6 +5,16 @@ frappe.provide("erpnext.projects"); frappe.ui.form.on("Time Log", "onload", function(frm) { frm.set_query("task", erpnext.queries.task); + if (frm.doc.time_log_for == "Manufacturing") { + frappe.ui.form.trigger("Time Log", "production_order"); + } +}); + +frappe.ui.form.on("Time Log", "refresh", function(frm) { + var is_manufacturing = frm.doc.time_log_for=="Manufacturing" ? true : false; + frm.toggle_reqd("production_order", is_manufacturing); + frm.toggle_reqd("operation", is_manufacturing); + frm.toggle_reqd("operation_status", is_manufacturing); }); // set to time if hours is updated @@ -45,7 +55,7 @@ $.extend(cur_frm.cscript, { $.each(doc.production_order_operations , function(i, row){ operations[i] = (i+1) +". "+ row.operation; }); - frappe.meta.get_docfield("Time Log", "operation", me.frm.doc.name).options = operations.join("\n"); + frappe.meta.get_docfield("Time Log", "operation", me.frm.doc.name).options = "\n" + operations.join("\n"); refresh_field("operation"); }) } @@ -60,14 +70,10 @@ $.extend(cur_frm.cscript, { }, callback: function(r) { if(!r.exc) { + console.log(r.message) cur_frm.set_value("workstation", r.message) } - doc.workstation = r.workstation; } }); } }); - -if (cur_frm.doc.time_log_for == "Manufacturing") { - cur_frm.cscript.onload = cur_frm.cscript.production_order; -} diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index 4afc0a0469..3b27ab8952 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -152,7 +152,7 @@ "read_only": 0 }, { - "depends_on": "eval:doc.time_log_for == 'Project'", + "depends_on": "eval:doc.time_log_for", "fieldname": "project", "fieldtype": "Link", "in_list_view": 1, @@ -200,7 +200,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2014-12-18 17:21:01.520646", + "modified": "2014-12-19 14:20:41.381152", "modified_by": "Administrator", "module": "Projects", "name": "Time Log", diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index db08abc852..541154a4a6 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -8,7 +8,6 @@ import frappe, json from frappe import _ from frappe.utils import cstr, comma_and, flt - class OverlapError(frappe.ValidationError): pass class OverProductionError(frappe.ValidationError): pass class NotSubmittedError(frappe.ValidationError): pass @@ -21,8 +20,10 @@ class TimeLog(Document): self.validate_overlap() self.validate_timings() self.calculate_total_hours() + self.validate_time_log_for() self.check_workstation_timings() self.validate_production_order() + self.validate_operation_status() def on_submit(self): self.update_production_order() @@ -74,6 +75,11 @@ class TimeLog(Document): from frappe.utils import time_diff_in_seconds self.hours = flt(time_diff_in_seconds(self.to_time, self.from_time)) / 3600 + def validate_time_log_for(self): + if self.time_log_for == "Project": + for fld in ["production_order", "operation", "workstation", "operation_status"]: + self.set(fld, None) + def check_workstation_timings(self): """Checks if **Time Log** is between operating hours of the **Workstation**.""" if self.workstation: @@ -82,28 +88,39 @@ class TimeLog(Document): def validate_production_order(self): """Throws 'NotSubmittedError' if **production order** is not submitted. """ - if self.production_order: if frappe.db.get_value("Production Order", self.production_order, "docstatus") != 1 : frappe.throw(_("You can make a time log only against a submitted production order"), NotSubmittedError) + def validate_operation_status(self): + if self.time_log_for=="Manufacturing" and self.production_order and self.operation: + if self.operation_status == "Work in Progress": + latest_time_log = self.get_latest_time_log() + if latest_time_log and latest_time_log[0].operation_status == "Completed": + frappe.throw("Operation is already completed via Time Log {}".format(latest_time_log[0].name)) + def update_production_order(self): """Updates `start_date`, `end_date`, `status` for operation in Production Order.""" if self.time_log_for=="Manufacturing" and self.operation: dates = self.get_operation_start_end_time() - op_status = self.get_op_status() - actual_op_time = self.get_actual_op_time() + latest_time_log = self.get_latest_time_log() + op_status = latest_time_log[0].operation_status if latest_time_log else "Pending" + + actual_op_time = self.get_actual_op_time() d = self.operation.split('. ',1) frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s, status = %s, actual_operation_time = %s where parent=%s and idx=%s and operation = %s""", - (dates.start_date, dates.end_date, op_status, actual_op_time, - self.production_order, d[0], d[1])) + (dates.start_date, dates.end_date, op_status, + actual_op_time, self.production_order, d[0], d[1])) - frappe.get_doc("Production Order", self.production_order).save() + pro_order = frappe.get_doc("Production Order", self.production_order) + pro_order.ignore_validate_update_after_submit = True + pro_order.calculate_operating_cost() + pro_order.save() def get_operation_start_end_time(self): """Returns Min From and Max To Dates of Time Logs against a specific Operation. """ @@ -118,12 +135,10 @@ class TimeLog(Document): (self.production_order, self.operation)) return actual_time[0][0] if actual_time else 0 - def get_op_status(self): - status = frappe.db.sql("""select operation_status from `tabTime Log` + def get_latest_time_log(self): + return frappe.db.sql("""select name, operation_status from `tabTime Log` where production_order=%s and operation=%s and docstatus=1 - order by to_time desc limit 1""", (self.production_order, self.operation)) - - return status if status else self.status + order by to_time desc limit 1""", (self.production_order, self.operation), as_dict=1) @frappe.whitelist() def get_workstation(production_order, operation): From 89c52eacc72b8013d256e8326f3587cdf6f1bac4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 23 Dec 2014 17:38:33 +0530 Subject: [PATCH 10/10] Actual operation cost in stock entry, fixed cost removed from bom and workstation --- erpnext/manufacturing/doctype/bom/bom.js | 23 +-- erpnext/manufacturing/doctype/bom/bom.json | 36 +--- erpnext/manufacturing/doctype/bom/bom.py | 46 +++--- .../manufacturing/doctype/bom/bom_list.html | 2 +- erpnext/manufacturing/doctype/bom/bom_list.js | 2 +- .../doctype/bom_operation/bom_operation.json | 17 +- .../bom_replace_tool/bom_replace_tool.py | 2 +- .../doctype/operation/operation.js | 14 -- .../production_order/production_order.js | 13 +- .../production_order/production_order.json | 30 ++-- .../production_order/production_order.py | 55 ++++--- .../production_order_operation.json | 154 ++++++++---------- .../doctype/workstation/test_records.json | 1 - .../doctype/workstation/workstation.js | 6 +- .../doctype/workstation/workstation.json | 33 +--- .../doctype/workstation/workstation.py | 7 +- erpnext/patches.txt | 1 - .../patches/v4_2/cost_of_production_cycle.py | 9 - erpnext/patches/v5_0/capacity_planning.py | 19 +-- erpnext/projects/doctype/time_log/time_log.js | 2 +- .../projects/doctype/time_log/time_log.json | 11 +- erpnext/projects/doctype/time_log/time_log.py | 41 ++--- .../stock/doctype/stock_entry/stock_entry.js | 2 - .../doctype/stock_entry/stock_entry.json | 10 +- .../stock/doctype/stock_entry/stock_entry.py | 44 ++++- .../doctype/stock_entry/test_stock_entry.py | 2 +- .../stock/report/item_prices/item_prices.py | 2 +- 27 files changed, 242 insertions(+), 342 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/operation/operation.js delete mode 100644 erpnext/patches/v4_2/cost_of_production_cycle.py diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 1fef504592..7a8d75a788 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -38,8 +38,7 @@ erpnext.bom.set_operation = function(doc) { operations[i] = (i+1); } - frappe.meta.get_docfield("BOM Item", "operation", - cur_frm.docname).options = operations.join("\n"); + frappe.meta.get_docfield("BOM Item", "operation", cur_frm.docname).options = operations.join("\n"); refresh_field("bom_materials"); } @@ -54,7 +53,6 @@ cur_frm.add_fetch("item", "stock_uom", "uom"); cur_frm.cscript.hour_rate = function(doc, dt, dn) { erpnext.bom.calculate_op_cost(doc); - erpnext.bom.calculate_fixed_cost(doc); erpnext.bom.calculate_total(doc); } @@ -114,16 +112,14 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) { erpnext.bom.calculate_op_cost = function(doc) { var op = doc.bom_operations || []; - doc.total_variable_cost, doc.total_fixed_cost = 0.0, 0.0; + doc.operating_cost = 0.0; for(var i=0;i 0""", args['item_code'], as_dict=1): + where item_code=%s""", args['item_code'], as_dict=1): total_qty += flt(d.actual_qty) total_value += flt(d.stock_value) - return total_value / total_qty if total_qty else 0.0 + if total_qty: + valuation_rate = total_value / total_qty + + if valuation_rate <= 0: + last_valuation_rate = frappe.db.sql("""select valuation_rate + from `tabStock Ledger Entry` + where item_code = %s and ifnull(valuation_rate, 0) > 0 + order by posting_date desc, posting_time desc, name desc limit 1""", args['item_code']) + + valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 + + return valuation_rate def manage_default_bom(self): """ Uncheck others if current one is selected as default, @@ -247,28 +257,20 @@ class BOM(Document): """Calculate bom totals""" self.calculate_op_cost() self.calculate_rm_cost() - self.total_cost = self.total_operating_cost + self.raw_material_cost + self.total_cost = self.operating_cost + self.raw_material_cost def calculate_op_cost(self): """Update workstation rate and calculates totals""" - total_variable_cost, total_fixed_cost = 0, 0 + self.operating_cost = 0 for d in self.get('bom_operations'): if d.workstation: - w = frappe.db.get_value("Workstation", d.workstation, ["hour_rate", "fixed_cost"]) if not d.hour_rate: - d.hour_rate = flt(w[0]) - - total_fixed_cost += flt(w[1]) + d.hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate")) if d.hour_rate and d.time_in_mins: - d.variable_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0 + d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0 - d.operating_cost = flt(d.fixed_cost) + flt(d.variable_cost) - total_variable_cost += flt(d.variable_cost) - - self.total_variable_cost = total_variable_cost - self.total_fixed_cost = total_fixed_cost - self.total_operating_cost = self.total_variable_cost + self.total_fixed_cost + self.operating_cost += flt(d.operating_cost) def calculate_rm_cost(self): """Fetch RM rate as per today's valuation rate and calculate totals""" diff --git a/erpnext/manufacturing/doctype/bom/bom_list.html b/erpnext/manufacturing/doctype/bom/bom_list.html index d3632a568f..8303f4a3d4 100644 --- a/erpnext/manufacturing/doctype/bom/bom_list.html +++ b/erpnext/manufacturing/doctype/bom/bom_list.html @@ -15,6 +15,6 @@
- {%= doc.get_formatted("total_variable_cost") %} + {%= doc.get_formatted("total_cost") %}
diff --git a/erpnext/manufacturing/doctype/bom/bom_list.js b/erpnext/manufacturing/doctype/bom/bom_list.js index 085e2dd0ea..71d54a20dc 100644 --- a/erpnext/manufacturing/doctype/bom/bom_list.js +++ b/erpnext/manufacturing/doctype/bom/bom_list.js @@ -1,3 +1,3 @@ frappe.listview_settings['BOM'] = { - add_fields: ["is_active", "is_default", "total_variable_cost"] + add_fields: ["is_active", "is_default", "total_cost"] }; diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 371f2a36be..a3678b54d6 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -40,13 +40,6 @@ "fieldtype": "Column Break", "permlevel": 0 }, - { - "fieldname": "fixed_cost", - "fieldtype": "Currency", - "in_list_view": 0, - "label": "Fixed Cost", - "permlevel": 0 - }, { "fieldname": "hour_rate", "fieldtype": "Float", @@ -69,14 +62,6 @@ "permlevel": 0, "reqd": 0 }, - { - "fieldname": "variable_cost", - "fieldtype": "Currency", - "label": "Variable Cost", - "permlevel": 0, - "precision": "", - "read_only": 1 - }, { "allow_on_submit": 0, "fieldname": "operating_cost", @@ -92,7 +77,7 @@ ], "idx": 1, "istable": 1, - "modified": "2014-12-17 17:54:34.313130", + "modified": "2014-12-23 15:01:54.340605", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/bom_replace_tool/bom_replace_tool.py b/erpnext/manufacturing/doctype/bom_replace_tool/bom_replace_tool.py index a5b2a53792..63030b588b 100644 --- a/erpnext/manufacturing/doctype/bom_replace_tool/bom_replace_tool.py +++ b/erpnext/manufacturing/doctype/bom_replace_tool/bom_replace_tool.py @@ -25,7 +25,7 @@ class BOMReplaceTool(Document): frappe.throw(_("Current BOM and New BOM can not be same")) def update_new_bom(self): - current_bom_unitcost = frappe.db.sql("""select total_variable_cost/quantity + current_bom_unitcost = frappe.db.sql("""select total_cost/quantity from `tabBOM` where name = %s""", self.current_bom) current_bom_unitcost = current_bom_unitcost and flt(current_bom_unitcost[0][0]) or 0 frappe.db.sql("""update `tabBOM Item` set bom_no=%s, diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js deleted file mode 100644 index 3c5053b8b7..0000000000 --- a/erpnext/manufacturing/doctype/operation/operation.js +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -frappe.provide("erpnext.operation"); - -$.extend(cur_frm.cscript, { - time_in_min: function(doc) { - doc.operating_cost = flt(doc.hour_rate) * flt(doc.time_in_min) / 60.0; - refresh_field('operating_cost'); - } -}); - -cur_frm.add_fetch('workstation', 'hour_rate', 'hour_rate'); -cur_frm.add_fetch('workstation', 'fixed_cycle_cost', 'fixed_cycle_cost'); diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index 5a6880945d..a949022db7 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -77,7 +77,8 @@ $.extend(cur_frm.cscript, { "from_time": child.planned_start_time, "to_time": child.planned_end_time, "project": doc.project, - "workstation": child.workstation + "workstation": child.workstation, + "qty": flt(doc.qty) - flt(child.completed_qty) }, callback: function(r) { var doclist = frappe.model.sync(r.message); @@ -164,11 +165,7 @@ cur_frm.set_query("bom_no", function(doc) { } else msgprint(__("Please enter Production Item first")); }); -cur_frm.add_fetch('bom_no', 'total_fixed_cost', 'total_fixed_cost'); -cur_frm.add_fetch('bom_no', 'total_variable_cost', 'planned_variable_cost'); -cur_frm.add_fetch('bom_no', 'total_operating_cost', 'total_operating_cost'); - -frappe.ui.form.on("Production Order", "total_fixed_cost", function(frm) { - var variable_cost = frm.doc.actual_variable_cost ? flt(frm.doc.actual_variable_cost) : flt(frm.doc.planned_variable_cost) - frm.set_value("total_operating_cost", (flt(frm.doc.total_fixed_cost) + variable_cost)) +frappe.ui.form.on("Production Order", "additional_operating_cost", function(frm) { + var variable_cost = frm.doc.actual_operating_cost ? flt(frm.doc.actual_operating_cost) : flt(frm.doc.planned_operating_cost) + frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost)) }) diff --git a/erpnext/manufacturing/doctype/production_order/production_order.json b/erpnext/manufacturing/doctype/production_order/production_order.json index da2bd5bf2a..9617ba0b6a 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -217,31 +217,34 @@ "precision": "" }, { - "depends_on": "", - "fieldname": "total_fixed_cost", + "fieldname": "planned_operating_cost", "fieldtype": "Currency", - "label": "Total Fixed Cost", - "options": "Company:company:default_currency", - "permlevel": 0 - }, - { - "fieldname": "planned_variable_cost", - "fieldtype": "Currency", - "label": "Planned Variable Cost", + "label": "Planned Operating Cost", + "no_copy": 0, "options": "Company:company:default_currency", "permlevel": 0, "precision": "", "read_only": 1 }, { - "fieldname": "actual_variable_cost", + "fieldname": "actual_operating_cost", "fieldtype": "Currency", - "label": "Actual Variable Cost", + "label": "Actual Operating Cost", + "no_copy": 1, "options": "Company:company:default_currency", "permlevel": 0, "precision": "", "read_only": 1 }, + { + "fieldname": "additional_operating_cost", + "fieldtype": "Currency", + "label": "Additional Operating Cost", + "no_copy": 1, + "options": "Company:company:default_currency", + "permlevel": 0, + "precision": "" + }, { "fieldname": "column_break_24", "fieldtype": "Column Break", @@ -252,6 +255,7 @@ "fieldname": "total_operating_cost", "fieldtype": "Currency", "label": "Total Operating Cost", + "no_copy": 1, "options": "Company:company:default_currency", "permlevel": 0, "precision": "", @@ -347,7 +351,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2014-12-19 14:23:50.701164", + "modified": "2014-12-23 15:07:26.516227", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index a075f74ad0..2c2c04def2 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -59,20 +59,15 @@ class ProductionOrder(Document): validate_warehouse_company(w, self.company) def calculate_operating_cost(self): - if self.total_fixed_cost==None: - self.total_fixed_cost = frappe.db.get_value("BOM", self.bom_no, "total_fixed_cost") - - self.planned_variable_cost, self.actual_variable_cost = 0.0, 0.0 + self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0 for d in self.get("production_order_operations"): + d.actual_operating_cost = flt(d.hour_rate) * flt(d.actual_operation_time) / 60 - d.actual_variable_cost = flt(d.hour_rate) * flt(d.actual_operation_time) / 60 - - self.planned_variable_cost += flt(d.variable_cost) - self.actual_variable_cost += flt(d.actual_variable_cost) - - variable_cost = self.actual_variable_cost if self.actual_variable_cost else self.planned_variable_cost - self.total_operating_cost = flt(self.total_fixed_cost) + flt(variable_cost) + self.planned_operating_cost += flt(d.planned_operating_cost) + self.actual_operating_cost += flt(d.actual_operating_cost) + variable_cost = self.actual_operating_cost if self.actual_operating_cost else self.planned_operating_cost + self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost) def validate_production_order_against_so(self): # already ordered qty @@ -171,15 +166,16 @@ class ProductionOrder(Document): self.set('production_order_operations', []) operations = frappe.db.sql("""select operation, opn_description, workstation, - hour_rate, time_in_mins, fixed_cost, variable_cost, "Pending" as status + hour_rate, time_in_mins, operating_cost as "planned_operating_cost", "Pending" as status from `tabBOM Operation` where parent = %s""", self.bom_no, as_dict=1) self.set('production_order_operations', operations) self.plan_operations() + self.calculate_operating_cost() def plan_operations(self): - scheduled_datetime = self.production_start_date + scheduled_datetime = self.planned_start_date for d in self.get('production_order_operations'): while getdate(scheduled_datetime) in self.get_holidays(d.workstation): scheduled_datetime = get_datetime(scheduled_datetime) + relativedelta(days=1) @@ -188,7 +184,7 @@ class ProductionOrder(Document): scheduled_datetime = get_datetime(scheduled_datetime) + relativedelta(minutes=d.time_in_mins) d.planned_end_time = scheduled_datetime - self.production_end_date = scheduled_datetime + self.planned_end_date = scheduled_datetime def get_holidays(self, workstation): @@ -202,6 +198,17 @@ class ProductionOrder(Document): return self.holidays[holiday_list] + def update_operation_status(self): + for d in self.get("production_order_operations"): + if not d.completed_qty: + d.status = "Pending" + elif flt(d.completed_qty) < flt(self.qty): + d.status = "Work in Progress" + elif flt(d.completed_qty) == flt(self.qty): + d.status = "Completed" + else: + frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) + @frappe.whitelist() def get_item_details(item): @@ -213,10 +220,7 @@ def get_item_details(item): return {} res = res[0] - bom = frappe.db.sql("""select name as bom_no,total_fixed_cost from `tabBOM` where item=%s - and ifnull(is_default, 0)=1""", item, as_dict=1) - if bom: - res.update(bom[0]) + res["bom_no"] = frappe.db.get_value("BOM", filters={"item": item, "is_default": 1}) return res @frappe.whitelist() @@ -228,6 +232,7 @@ def make_stock_entry(production_order_id, purpose, qty=None): stock_entry.production_order = production_order_id stock_entry.company = production_order.company stock_entry.bom_no = production_order.bom_no + stock_entry.additional_operating_cost = production_order.additional_operating_cost stock_entry.use_multi_level_bom = production_order.use_multi_level_bom stock_entry.fg_completed_qty = qty or (flt(production_order.qty) - flt(production_order.produced_qty)) @@ -267,7 +272,7 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_time_log(name, operation, from_time, to_time, qty=None, project=None, workstation=None): +def make_time_log(name, operation, from_time, to_time, qty=None, project=None, workstation=None): time_log = frappe.new_doc("Time Log") time_log.time_log_for = 'Manufacturing' time_log.from_time = from_time @@ -276,16 +281,24 @@ def make_time_log(name, operation, from_time, to_time, qty=None, project=None, w time_log.project = project time_log.operation= operation time_log.workstation= workstation + time_log.completed_qty = flt(qty) if from_time and to_time : time_log.calculate_total_hours() return time_log @frappe.whitelist() def auto_make_time_log(production_order_id): + if frappe.db.get_value("Time Log", filters={"production_order": production_order_id}): + frappe.throw(_("Time logs already exists against this Production Order")) + + time_logs = [] prod_order = frappe.get_doc("Production Order", production_order_id) + for d in prod_order.production_order_operations: operation = cstr(d.idx) + ". " + d.operation time_log = make_time_log(prod_order.name, operation, d.planned_start_time, d.planned_end_time, - prod_order.qty, prod_order.project_name, d.workstation) + flt(prod_order.qty) - flt(d.completed_qty), prod_order.project_name, d.workstation) time_log.save() - frappe.msgprint(_("Time Logs created.")) + time_logs.append(time_log.name) + if time_logs: + frappe.msgprint(_("Time Logs created:") + "\n" + "\n".join(time_logs)) diff --git a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json index f17a055970..f495b0bc19 100644 --- a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json @@ -79,12 +79,23 @@ "set_only_once": 0, "unique": 0 }, + { + "description": "Operation completed for how many finished goods?", + "fieldname": "completed_qty", + "fieldtype": "Float", + "label": "Completed Qty", + "no_copy": 1, + "permlevel": 0, + "precision": "", + "read_only": 1 + }, { "default": "Pending", "fieldname": "status", "fieldtype": "Select", "in_list_view": 1, "label": "Status", + "no_copy": 1, "options": "Pending\nWork in Progress\nCompleted", "permlevel": 0, "precision": "", @@ -121,57 +132,24 @@ "precision": "" }, { - "allow_on_submit": 0, - "fieldname": "hour_rate", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, + "fieldname": "planned_start_time", + "fieldtype": "Datetime", "in_list_view": 0, - "label": "Hour Rate", - "no_copy": 0, - "oldfieldname": "hour_rate", - "oldfieldtype": "Currency", + "label": "Planned Start Time", + "no_copy": 1, "permlevel": 0, "precision": "", - "print_hide": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "reqd": 1 }, { - "allow_on_submit": 0, - "fieldname": "fixed_cost", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, + "fieldname": "planned_end_time", + "fieldtype": "Datetime", "in_list_view": 0, - "label": "Fixed Cost", - "no_copy": 0, - "options": "Company:company:default_currency", + "label": "Planned End Time", + "no_copy": 1, "permlevel": 0, "precision": "", - "print_hide": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "fieldname": "variable_cost", - "fieldtype": "Currency", - "label": "Variable Cost", - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "read_only": 1 + "reqd": 1 }, { "fieldname": "column_break_10", @@ -203,56 +181,41 @@ "unique": 0 }, { - "fieldname": "planned_start_time", - "fieldtype": "Datetime", - "in_list_view": 0, - "label": "Planned Start Time", - "no_copy": 1, - "permlevel": 0, - "precision": "", - "reqd": 1 - }, - { - "fieldname": "planned_end_time", - "fieldtype": "Datetime", - "in_list_view": 0, - "label": "Planned End Time", - "no_copy": 1, - "permlevel": 0, - "precision": "", - "reqd": 1 - }, - { - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "label": "Actual Time and Cost", - "permlevel": 0, - "precision": "" - }, - { - "description": "in Minutes\nUpdated via 'Time Log'", - "fieldname": "actual_operation_time", + "allow_on_submit": 0, + "fieldname": "hour_rate", "fieldtype": "Float", - "label": "Actual Operation Time", - "no_copy": 1, + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Hour Rate", + "no_copy": 0, + "oldfieldname": "hour_rate", + "oldfieldtype": "Currency", "permlevel": 0, "precision": "", - "read_only": 1 + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 }, { - "description": "Hour Rate * Actual Operating Cost", - "fieldname": "actual_variable_cost", + "fieldname": "planned_operating_cost", "fieldtype": "Currency", - "label": "Actual Variable Cost", - "no_copy": 1, + "label": "Planned Operating Cost", + "no_copy": 0, "options": "Company:company:default_currency", "permlevel": 0, "precision": "", "read_only": 1 }, { - "fieldname": "column_break_11", - "fieldtype": "Column Break", + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Actual Time and Cost", "permlevel": 0, "precision": "" }, @@ -275,6 +238,33 @@ "precision": "", "read_only": 1 }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "description": "in Minutes\nUpdated via 'Time Log'", + "fieldname": "actual_operation_time", + "fieldtype": "Float", + "label": "Actual Operation Time", + "no_copy": 1, + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "description": "Hour Rate * Actual Operating Cost", + "fieldname": "actual_operating_cost", + "fieldtype": "Currency", + "label": "Actual Operating Cost", + "no_copy": 1, + "options": "Company:company:default_currency", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, { "allow_on_submit": 1, "depends_on": "eval:(doc.docstatus==1 && doc.status!=\"Completed\")", @@ -292,7 +282,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2014-12-19 12:49:49.918120", + "modified": "2014-12-23 15:42:34.892964", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order Operation", diff --git a/erpnext/manufacturing/doctype/workstation/test_records.json b/erpnext/manufacturing/doctype/workstation/test_records.json index 685c84e2d7..37fb5aa4e4 100644 --- a/erpnext/manufacturing/doctype/workstation/test_records.json +++ b/erpnext/manufacturing/doctype/workstation/test_records.json @@ -4,7 +4,6 @@ "name": "_Test Workstation 1", "workstation_name": "_Test Workstation 1", "warehouse": "_Test warehouse - _TC", - "fixed_cycle_cost": 1000, "hour_rate":100, "holiday_list": "_Test Holiday List", "workstation_operation_hours": [ diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index d3c7b56e8a..17320520af 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -1,7 +1,7 @@ // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - + //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { @@ -15,7 +15,3 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) { } }) } - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 37932bc78c..ecf0f667a5 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -60,19 +60,6 @@ "permlevel": 0, "precision": "" }, - { - "fieldname": "fixed_costs", - "fieldtype": "Section Break", - "label": "Fixed Costs", - "permlevel": 0, - "precision": "" - }, - { - "fieldname": "fixed_cost", - "fieldtype": "Float", - "label": "Fixed Cost", - "permlevel": 0 - }, { "fieldname": "over_heads", "fieldtype": "Section Break", @@ -98,15 +85,6 @@ "oldfieldtype": "Currency", "permlevel": 0 }, - { - "description": "per hour", - "fieldname": "hour_rate_rent", - "fieldtype": "Float", - "label": "Rent Cost", - "oldfieldname": "hour_rate_rent", - "oldfieldtype": "Currency", - "permlevel": 0 - }, { "fieldname": "column_break_11", "fieldtype": "Column Break", @@ -115,13 +93,12 @@ }, { "description": "per hour", - "fieldname": "total_variable_cost", + "fieldname": "hour_rate_rent", "fieldtype": "Float", - "label": "Total Variable Cost", - "oldfieldname": "overhead", + "label": "Rent Cost", + "oldfieldname": "hour_rate_rent", "oldfieldtype": "Currency", - "permlevel": 0, - "read_only": 1 + "permlevel": 0 }, { "description": "Wages per hour", @@ -161,7 +138,7 @@ ], "icon": "icon-wrench", "idx": 1, - "modified": "2014-12-22 14:18:40.253034", + "modified": "2014-12-23 15:27:58.477925", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index b95ca45dd7..62819774c7 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -25,9 +25,8 @@ class Workstation(Document): def on_update(self): self.validate_overlap_for_operation_timings() - frappe.db.set(self, 'total_variable_cost', flt(self.hour_rate_electricity) + + frappe.db.set(self, 'hour_rate', flt(self.hour_rate_labour) + flt(self.hour_rate_electricity) + flt(self.hour_rate_consumable) + flt(self.hour_rate_rent)) - frappe.db.set(self, 'hour_rate', flt(self.hour_rate_labour) + flt(self.total_variable_cost)) self.update_bom_operation() @@ -41,8 +40,8 @@ class Workstation(Document): (%s between start_time and end_time)) """, (self.name, d.name, d.start_time, d.end_time, d.start_time, d.end_time, d.start_time)) - if existing: - frappe.throw(_("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError) + if existing: + frappe.throw(_("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError) @frappe.whitelist() def get_default_holiday_list(): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 07cfdbf040..4997ede2cb 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -80,7 +80,6 @@ execute:frappe.delete_doc("DocType", "Landed Cost Wizard") erpnext.patches.v4_2.default_website_style erpnext.patches.v4_2.set_company_country erpnext.patches.v4_2.update_sales_order_invoice_field_name -erpnext.patches.v4_2.cost_of_production_cycle erpnext.patches.v4_2.seprate_manufacture_and_repack execute:frappe.delete_doc("Report", "Warehouse-Wise Stock Balance") execute:frappe.delete_doc("DocType", "Purchase Request") diff --git a/erpnext/patches/v4_2/cost_of_production_cycle.py b/erpnext/patches/v4_2/cost_of_production_cycle.py deleted file mode 100644 index 26f0fcad5b..0000000000 --- a/erpnext/patches/v4_2/cost_of_production_cycle.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe - -def execute(): - frappe.reload_doc("manufacturing", "doctype", "bom") - frappe.db.sql("""update tabBOM set total_variable_cost = total_cost""") \ No newline at end of file diff --git a/erpnext/patches/v5_0/capacity_planning.py b/erpnext/patches/v5_0/capacity_planning.py index ce964353b0..291fc5c782 100644 --- a/erpnext/patches/v5_0/capacity_planning.py +++ b/erpnext/patches/v5_0/capacity_planning.py @@ -4,20 +4,5 @@ import frappe def execute(): - for dt in ["workstation", "bom", "bom_operation"]: - frappe.reload_doc("manufacturing", "doctype", dt) - - frappe.db.sql("update `tabWorkstation` set fixed_cost = fixed_cycle_cost, total_variable_cost = overhead") - - frappe.db.sql("update `tabBOM Operation` set fixed_cost = fixed_cycle_cost") - - for d in frappe.db.sql("select name from `tabBOM` where docstatus < 2"): - try: - bom = frappe.get_doc('BOM', d[0]) - if bom.docstatus == 1: - bom.ignore_validate_update_after_submit = True - bom.calculate_cost() - bom.save() - except: - print "error", frappe.get_traceback() - pass + frappe.reload_doc("stock", "doctype", "stock_entry") + frappe.db.sql("update tabBOM set additional_operating_cost = total_fixed_cost") diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index e255843872..b23b3eadf4 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -14,7 +14,7 @@ frappe.ui.form.on("Time Log", "refresh", function(frm) { var is_manufacturing = frm.doc.time_log_for=="Manufacturing" ? true : false; frm.toggle_reqd("production_order", is_manufacturing); frm.toggle_reqd("operation", is_manufacturing); - frm.toggle_reqd("operation_status", is_manufacturing); + frm.toggle_reqd("completed_qty", is_manufacturing); }); // set to time if hours is updated diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index 3b27ab8952..343a697b10 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -116,11 +116,10 @@ "read_only": 1 }, { - "default": "Work in Progress", - "fieldname": "operation_status", - "fieldtype": "Select", - "label": "Operation Status", - "options": "\nWork in Progress\nCompleted", + "description": "Operation completed for how many finished goods?", + "fieldname": "completed_qty", + "fieldtype": "Float", + "label": "Completed Qty", "permlevel": 0, "precision": "" }, @@ -200,7 +199,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2014-12-19 14:20:41.381152", + "modified": "2014-12-22 15:22:00.664972", "modified_by": "Administrator", "module": "Projects", "name": "Time Log", diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index 541154a4a6..52257437b3 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -23,7 +23,6 @@ class TimeLog(Document): self.validate_time_log_for() self.check_workstation_timings() self.validate_production_order() - self.validate_operation_status() def on_submit(self): self.update_production_order() @@ -77,7 +76,7 @@ class TimeLog(Document): def validate_time_log_for(self): if self.time_log_for == "Project": - for fld in ["production_order", "operation", "workstation", "operation_status"]: + for fld in ["production_order", "operation", "workstation", "completed_qty"]: self.set(fld, None) def check_workstation_timings(self): @@ -92,33 +91,25 @@ class TimeLog(Document): if frappe.db.get_value("Production Order", self.production_order, "docstatus") != 1 : frappe.throw(_("You can make a time log only against a submitted production order"), NotSubmittedError) - def validate_operation_status(self): - if self.time_log_for=="Manufacturing" and self.production_order and self.operation: - if self.operation_status == "Work in Progress": - latest_time_log = self.get_latest_time_log() - if latest_time_log and latest_time_log[0].operation_status == "Completed": - frappe.throw("Operation is already completed via Time Log {}".format(latest_time_log[0].name)) - def update_production_order(self): """Updates `start_date`, `end_date`, `status` for operation in Production Order.""" if self.time_log_for=="Manufacturing" and self.operation: + operation = self.operation.split('. ',1) + dates = self.get_operation_start_end_time() + tl = self.get_all_time_logs() - latest_time_log = self.get_latest_time_log() - op_status = latest_time_log[0].operation_status if latest_time_log else "Pending" - - actual_op_time = self.get_actual_op_time() - d = self.operation.split('. ',1) frappe.db.sql("""update `tabProduction Order Operation` - set actual_start_time = %s, actual_end_time = %s, status = %s, actual_operation_time = %s + set actual_start_time = %s, actual_end_time = %s, completed_qty = %s, actual_operation_time = %s where parent=%s and idx=%s and operation = %s""", - (dates.start_date, dates.end_date, op_status, - actual_op_time, self.production_order, d[0], d[1])) + (dates.start_date, dates.end_date, tl.completed_qty, + tl.hours, self.production_order, operation[0], operation[1])) pro_order = frappe.get_doc("Production Order", self.production_order) pro_order.ignore_validate_update_after_submit = True + pro_order.update_operation_status() pro_order.calculate_operating_cost() pro_order.save() @@ -128,17 +119,13 @@ class TimeLog(Document): where production_order = %s and operation = %s and docstatus=1""", (self.production_order, self.operation), as_dict=1)[0] - def get_actual_op_time(self): + def get_all_time_logs(self): """Returns 'Actual Operating Time'. """ - actual_time = frappe.db.sql("""select sum(hours*60) as time_diff from - `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", - (self.production_order, self.operation)) - return actual_time[0][0] if actual_time else 0 - - def get_latest_time_log(self): - return frappe.db.sql("""select name, operation_status from `tabTime Log` - where production_order=%s and operation=%s and docstatus=1 - order by to_time desc limit 1""", (self.production_order, self.operation), as_dict=1) + return frappe.db.sql("""select + sum(hours*60) as hours, sum(ifnull(completed_qty, 0)) as completed_qty + from `tabTime Log` + where production_order = %s and operation = %s and docstatus=1""", + (self.production_order, self.operation), as_dict=1)[0] @frappe.whitelist() def get_workstation(production_order, operation): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index d961b40f62..b5bd455b09 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -461,8 +461,6 @@ cur_frm.fields_dict.customer.get_query = function(doc, cdt, cdn) { cur_frm.fields_dict.supplier.get_query = function(doc, cdt, cdn) { return { query: "erpnext.controllers.queries.supplier_query" } } -cur_frm.add_fetch('production_order', 'total_fixed_cost', 'total_fixed_cost'); -cur_frm.add_fetch('bom_no', 'total_fixed_cost', 'total_fixed_cost'); cur_frm.cscript.company = function(doc, cdt, cdn) { erpnext.get_fiscal_year(doc.company, doc.posting_date); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 8cbfbef1ea..8630d0376a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -299,9 +299,11 @@ }, { "depends_on": "eval:inList([\"Manufacture\", \"Repack\"], doc.purpose)", - "fieldname": "total_fixed_cost", - "fieldtype": "Float", - "label": "Total Fixed Cost", + "fieldname": "additional_operating_cost", + "fieldtype": "Currency", + "label": "Additional Operating Cost", + "no_copy": 1, + "options": "Company:company:default_currency", "permlevel": 0, "read_only": 0 }, @@ -585,7 +587,7 @@ "is_submittable": 1, "issingle": 0, "max_attachments": 0, - "modified": "2014-10-03 14:55:44.916658", + "modified": "2014-12-23 15:03:42.963697", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a9a670dedd..cb21867661 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -171,10 +171,20 @@ class StockEntry(StockController): if not self.production_order: frappe.throw(_("Production order number is mandatory for stock entry purpose manufacture")) # check for double entry + self.check_if_operations_completed() self.check_duplicate_entry_for_production_order() elif self.purpose != "Material Transfer": self.production_order = None + def check_if_operations_completed(self): + prod_order = frappe.get_doc("Production Order", self.production_order) + if prod_order.actual_operating_cost: + for d in prod_order.get("production_order_operations"): + total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) + if total_completed_qty > flt(d.completed_qty): + frappe.throw(_("Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Production Order # {3}. Please update operation status via Time Logs") + .format(d.idx, d.operation, total_completed_qty, self.production_order)) + def check_duplicate_entry_for_production_order(self): other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", { "production_order": self.production_order, @@ -258,14 +268,28 @@ class StockEntry(StockController): for d in self.get("mtn_details"): if d.bom_no or (d.t_warehouse and number_of_fg_items == 1): if not flt(d.incoming_rate) or force: - operation_cost_per_unit = 0 - if d.bom_no: - bom = frappe.db.get_value("BOM", d.bom_no, ["operating_cost", "quantity"], as_dict=1) - operation_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) - d.incoming_rate = operation_cost_per_unit + (raw_material_cost + flt(self.total_fixed_cost)) / flt(d.transfer_qty) + operation_cost_per_unit = self.get_operation_cost_per_unit(d.bom_no, d.qty) + d.incoming_rate = operation_cost_per_unit + (raw_material_cost / flt(d.transfer_qty)) d.amount = flt(d.transfer_qty) * flt(d.incoming_rate) break + def get_operation_cost_per_unit(self, bom_no, qty): + operation_cost_per_unit = 0 + + if self.production_order: + pro_order = frappe.get_doc("Production Order", self.production_order) + for d in pro_order.get("production_order_operations"): + if flt(d.completed_qty): + operation_cost_per_unit += flt(d.actual_operating_cost) / flt(d.completed_qty) + else: + operation_cost_per_unit += flt(d.planned_operating_cost) / flt(self.qty) + + if not operation_cost_per_unit and bom_no: + bom = frappe.db.get_value("BOM", bom_no, ["operating_cost", "quantity"], as_dict=1) + operation_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) + + return operation_cost_per_unit + flt(self.additional_operating_cost) / flt(qty) + def get_incoming_rate(self, args): incoming_rate = 0 if self.purpose == "Sales Return": @@ -643,10 +667,12 @@ def get_party_details(ref_dt, ref_dn): @frappe.whitelist() def get_production_order_details(production_order): - result = frappe.db.sql("""select bom_no, - ifnull(qty, 0) - ifnull(produced_qty, 0) as fg_completed_qty, use_multi_level_bom, - wip_warehouse from `tabProduction Order` where name = %s""", production_order, as_dict=1) - return result and result[0] or {} + res = frappe.db.sql("""select bom_no, use_multi_level_bom, wip_warehouse, + ifnull(qty, 0) - ifnull(produced_qty, 0) as fg_completed_qty, + (infull(additional_operating_cost, 0) / qty)*(ifnull(qty, 0) - ifnull(produced_qty, 0)) as additional_operating_cost + from `tabProduction Order` where name = %s""", production_order, as_dict=1) + + return res and res[0] or {} def query_sales_return_doc(doctype, txt, searchfield, start, page_len, filters): conditions = "" diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 7caf0627bf..cc539f6ef3 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -918,7 +918,7 @@ class TestStockEntry(unittest.TestCase): "production_order": production_order.name, "bom_no": bom_no, "fg_completed_qty": "1", - "total_fixed_cost": 1000 + "additional_operating_cost": 1000 }) stock_entry.get_items() diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index 2b413fd29b..bd885b94f4 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -115,7 +115,7 @@ def get_item_bom_rate(): item_bom_map = {} - for b in frappe.db.sql("""select item, (total_variable_cost/quantity) as bom_rate + for b in frappe.db.sql("""select item, (total_cost/quantity) as bom_rate from `tabBOM` where is_active=1 and is_default=1""", as_dict=1): item_bom_map.setdefault(b.item, flt(b.bom_rate))