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) { ] } }); - diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index a1644a2da9..6c915b7a81 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -25,7 +25,17 @@ 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/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/hr/doctype/holiday_list/test_records.json b/erpnext/hr/doctype/holiday_list/test_records.json index 9ef8c8efde..be7b178112 100644 --- a/erpnext/hr/doctype/holiday_list/test_records.json +++ b/erpnext/hr/doctype/holiday_list/test_records.json @@ -1,15 +1,15 @@ [ { - "doctype": "Holiday List", + "doctype": "Holiday List", "fiscal_year": "_Test Fiscal Year 2013", "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..7a8d75a788 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,36 @@ 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) { - var op_table = doc.bom_operations || []; +erpnext.bom.set_operation = function(doc) { + var op_table = doc["bom_operations"] || []; var operations = []; for (var i=0, j=op_table.length; 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, @@ -171,7 +180,7 @@ class BOM(Document): if not self.with_operations: self.set('bom_operations', []) for d in self.get("bom_materials"): - d.operation_no = None + d.operation = None def validate_main_item(self): """ Validate main FG item""" @@ -183,23 +192,10 @@ class BOM(Document): self.description = ret[0] self.uom = ret[1] - def validate_operations(self): - """ Check duplicate operation no""" - self.op = [] - for d in self.get('bom_operations'): - if cstr(d.operation_no) in self.op: - frappe.throw(_("Operation {0} is repeated in Operations Table").format(d.operation_no)) - else: - # add operation in op list - self.op.append(cstr(d.operation_no)) - def validate_materials(self): """ Validate raw material entries """ check_list = [] for m in self.get('bom_materials'): - # check if operation no not in op table - if self.with_operations and cstr(m.operation_no) not in self.op: - frappe.throw(_("Operation {0} not present in Operations Table").format(m.operation_no)) if m.bom_no: validate_bom_no(m.item_code, m.bom_no) @@ -207,7 +203,7 @@ class BOM(Document): if flt(m.qty) <= 0: frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx)) - self.check_if_item_repeated(m.item_code, m.operation_no, check_list) + self.check_if_item_repeated(m.item_code, m.operation, check_list) def check_if_item_repeated(self, item, op, check_list): @@ -261,26 +257,20 @@ class BOM(Document): """Calculate bom totals""" self.calculate_op_cost() self.calculate_rm_cost() - self.total_variable_cost = self.raw_material_cost + self.operating_cost - self.total_cost = self.total_variable_cost + self.total_fixed_cost + self.total_cost = self.operating_cost + self.raw_material_cost def calculate_op_cost(self): """Update workstation rate and calculates totals""" - total_op_cost, 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_cycle_cost"]) if not d.hour_rate: - d.hour_rate = flt(w[0]) - - 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.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0 - total_op_cost += flt(d.operating_cost) - self.operating_cost = total_op_cost - self.total_fixed_cost = 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""" @@ -426,4 +416,3 @@ def validate_bom_no(item, bom_no): if item and not (bom.item == item or \ bom.item == frappe.db.get_value("Item", item, "variant_of")): frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) - 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/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json index 26aa6d3b16..56e3abfeed 100644 --- a/erpnext/manufacturing/doctype/bom/test_records.json +++ b/erpnext/manufacturing/doctype/bom/test_records.json @@ -58,7 +58,7 @@ { "bom_operations": [ { - "operation_no": "1", + "operation": "_Test Operation 1", "opn_description": "_Test", "workstation": "_Test Workstation 1", "time_in_min": 60, @@ -67,7 +67,7 @@ ], "bom_materials": [ { - "operation_no": 1, + "operation": 1, "amount": 5000.0, "doctype": "BOM Item", "item_code": "_Test Item", @@ -77,7 +77,7 @@ "stock_uom": "_Test UOM" }, { - "operation_no": 1, + "operation": 1, "amount": 2000.0, "bom_no": "BOM/_Test Item Home Desktop Manufactured/001", "doctype": "BOM Item", @@ -99,7 +99,7 @@ { "bom_operations": [ { - "operation_no": "1", + "operation": "_Test Operation 1", "opn_description": "_Test", "workstation": "_Test Workstation 1", "time_in_min": 60, @@ -108,7 +108,7 @@ ], "bom_materials": [ { - "operation_no": 1, + "operation": 1, "amount": 5000.0, "doctype": "BOM Item", "item_code": "_Test Item", diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 39ceaa707f..872f532559 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -1,147 +1,147 @@ { - "creation": "2013-02-22 01:27:49", - "docstatus": 0, - "doctype": "DocType", + "creation": "2013-02-22 01:27:49", + "docstatus": 0, + "doctype": "DocType", "fields": [ { - "fieldname": "operation_no", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Operation No", - "oldfieldname": "operation_no", - "oldfieldtype": "Data", - "permlevel": 0, + "fieldname": "operation", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Operation", + "oldfieldname": "operation_no", + "oldfieldtype": "Data", + "permlevel": 0, "reqd": 0 - }, + }, { - "fieldname": "item_code", - "fieldtype": "Link", - "in_filter": 1, - "in_list_view": 1, - "label": "Item Code", - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "permlevel": 0, - "reqd": 1, + "fieldname": "item_code", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "permlevel": 0, + "reqd": 1, "search_index": 1 - }, + }, { - "fieldname": "bom_no", - "fieldtype": "Link", - "in_filter": 1, - "in_list_view": 1, - "label": "BOM No", - "oldfieldname": "bom_no", - "oldfieldtype": "Link", - "options": "BOM", - "permlevel": 0, - "print_width": "150px", - "reqd": 0, - "search_index": 1, + "fieldname": "bom_no", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "BOM No", + "oldfieldname": "bom_no", + "oldfieldtype": "Link", + "options": "BOM", + "permlevel": 0, + "print_width": "150px", + "reqd": 0, + "search_index": 1, "width": "150px" - }, + }, { - "fieldname": "col_break1", - "fieldtype": "Column Break", + "fieldname": "col_break1", + "fieldtype": "Column Break", "permlevel": 0 - }, + }, { - "fieldname": "description", - "fieldtype": "Text", - "label": "Item Description", - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "print_width": "250px", - "reqd": 0, + "fieldname": "description", + "fieldtype": "Text", + "label": "Item Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "permlevel": 0, + "print_width": "250px", + "reqd": 0, "width": "250px" - }, + }, { - "fieldname": "quantity_and_rate", - "fieldtype": "Section Break", - "label": "Quantity and Rate", + "fieldname": "quantity_and_rate", + "fieldtype": "Section Break", + "label": "Quantity and Rate", "permlevel": 0 - }, + }, { - "fieldname": "qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Qty", - "oldfieldname": "qty", - "oldfieldtype": "Currency", - "permlevel": 0, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "oldfieldname": "qty", + "oldfieldtype": "Currency", + "permlevel": 0, "reqd": 1 - }, + }, { - "description": "See \"Rate Of Materials Based On\" in Costing Section", - "fieldname": "rate", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Rate", - "options": "Company:company:default_currency", - "permlevel": 0, + "description": "See \"Rate Of Materials Based On\" in Costing Section", + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "Company:company:default_currency", + "permlevel": 0, "reqd": 1 - }, + }, { - "fieldname": "col_break2", - "fieldtype": "Column Break", + "fieldname": "col_break2", + "fieldtype": "Column Break", "permlevel": 0 - }, + }, { - "fieldname": "stock_uom", - "fieldtype": "Link", - "in_list_view": 0, - "label": "Stock UOM", - "oldfieldname": "stock_uom", - "oldfieldtype": "Data", - "options": "UOM", - "permlevel": 0, - "read_only": 1, + "fieldname": "stock_uom", + "fieldtype": "Link", + "in_list_view": 0, + "label": "Stock UOM", + "oldfieldname": "stock_uom", + "oldfieldtype": "Data", + "options": "UOM", + "permlevel": 0, + "read_only": 1, "reqd": 1 - }, + }, { - "fieldname": "amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Amount", - "oldfieldname": "amount_as_per_mar", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "print_width": "150px", - "read_only": 1, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "oldfieldname": "amount_as_per_mar", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "permlevel": 0, + "print_width": "150px", + "read_only": 1, "width": "150px" - }, + }, { - "fieldname": "scrap", - "fieldtype": "Float", - "label": "Scrap %", - "oldfieldname": "scrap", - "oldfieldtype": "Currency", - "permlevel": 0, + "fieldname": "scrap", + "fieldtype": "Float", + "label": "Scrap %", + "oldfieldname": "scrap", + "oldfieldtype": "Currency", + "permlevel": 0, "print_hide": 1 - }, + }, { - "fieldname": "qty_consumed_per_unit", - "fieldtype": "Float", - "hidden": 1, - "label": "Qty Consumed Per Unit", - "oldfieldname": "qty_consumed_per_unit", - "oldfieldtype": "Float", - "permlevel": 0, - "print_hide": 1, + "fieldname": "qty_consumed_per_unit", + "fieldtype": "Float", + "hidden": 1, + "label": "Qty Consumed Per Unit", + "oldfieldname": "qty_consumed_per_unit", + "oldfieldtype": "Float", + "permlevel": 0, + "print_hide": 1, "read_only": 1 } - ], - "idx": 1, - "istable": 1, - "modified": "2014-12-12 11:15:43.798755", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Item", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", + ], + "idx": 1, + "istable": 1, + "modified": "2014-12-12 11:15:43.798755", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index d4dd95850b..a3678b54d6 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -4,15 +4,27 @@ "doctype": "DocType", "fields": [ { - "fieldname": "operation_no", - "fieldtype": "Data", + "fieldname": "operation", + "fieldtype": "Link", "in_list_view": 1, - "label": "Operation No", + "label": "Operation", "oldfieldname": "operation_no", "oldfieldtype": "Data", + "options": "Operation", "permlevel": 0, "reqd": 1 }, + { + "fieldname": "workstation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Workstation", + "oldfieldname": "workstation", + "oldfieldtype": "Link", + "options": "Workstation", + "permlevel": 0, + "reqd": 0 + }, { "fieldname": "opn_description", "fieldtype": "Text", @@ -28,35 +40,25 @@ "fieldtype": "Column Break", "permlevel": 0 }, - { - "fieldname": "workstation", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Workstation", - "oldfieldname": "workstation", - "oldfieldtype": "Link", - "options": "Workstation", - "permlevel": 0, - "reqd": 0 - }, { "fieldname": "hour_rate", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 0, "label": "Hour Rate", "oldfieldname": "hour_rate", "oldfieldtype": "Currency", - "options": "Company:company:default_currency", "permlevel": 0, "reqd": 0 }, { + "description": "In minutes", "fieldname": "time_in_mins", "fieldtype": "Float", "in_list_view": 0, - "label": "Operation Time (mins)", + "label": "Operation Time ", "oldfieldname": "time_in_mins", "oldfieldtype": "Currency", + "options": "", "permlevel": 0, "reqd": 0 }, @@ -68,22 +70,14 @@ "label": "Operating Cost", "oldfieldname": "operating_cost", "oldfieldtype": "Currency", - "options": "Company:company:default_currency", "permlevel": 0, + "read_only": 1, "reqd": 0 - }, - { - "fieldname": "fixed_cycle_cost", - "fieldtype": "Currency", - "in_list_view": 0, - "label": "Fixed Cycle Cost", - "options": "Company:company:default_currency", - "permlevel": 0 } ], "idx": 1, "istable": 1, - "modified": "2014-12-12 11:16:49.031521", + "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/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..a5bdecd8db --- /dev/null +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -0,0 +1,84 @@ +{ + "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": [ + { + "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": "" + }, + { + "default": "", + "fieldname": "allow_production_on_holidays", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Allow Production on Holidays", + "options": "", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "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", + "in_list_view": 1, + "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-22 12:43:15.261503", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "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/__init__.py b/erpnext/manufacturing/doctype/operation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json new file mode 100644 index 0000000000..8533648a0c --- /dev/null +++ b/erpnext/manufacturing/doctype/operation/operation.json @@ -0,0 +1,109 @@ +{ + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:operation", + "creation": "2014-11-07 16:20:30.683186", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "operation", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Operation", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "workstation", + "fieldtype": "Link", + "label": "Default Workstation", + "options": "Workstation", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "opn_description", + "fieldtype": "Text", + "label": "Operation Description", + "permlevel": 0, + "precision": "" + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-wrench", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "modified": "2014-12-18 16:21:59.462435", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Operation", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 0, + "export": 1, + "import": 1, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "Manufacturing User", + "set_user_permissions": 0, + "submit": 0, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "export": 1, + "import": 1, + "permlevel": 0, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "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/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py new file mode 100644 index 0000000000..c35731ca7a --- /dev/null +++ b/erpnext/manufacturing/doctype/operation/operation.py @@ -0,0 +1,14 @@ +# 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 Operation(Document): + def calculate_op_cost(self): + if self.hour_rate and self.time_in_mins: + self.operating_cost = flt(self.hour_rate) * flt(self.time_in_mins) / 60.0 + else : + self.operating_cost = 0 + diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py new file mode 100644 index 0000000000..daa450d89c --- /dev/null +++ b/erpnext/manufacturing/doctype/operation/test_operation.py @@ -0,0 +1,10 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +import unittest + +test_records = frappe.get_test_records('Operation') + +class TestOperation(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/operation/test_records.json b/erpnext/manufacturing/doctype/operation/test_records.json new file mode 100644 index 0000000000..56da6d33d5 --- /dev/null +++ b/erpnext/manufacturing/doctype/operation/test_records.json @@ -0,0 +1,7 @@ +[ + { + "doctype": "Operation", + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1" + } +] diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index fbb9a269c1..a949022db7 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -3,7 +3,6 @@ $.extend(cur_frm.cscript, { onload: function (doc, dt, dn) { - if (!doc.status) doc.status = 'Draft'; cfn_set_fields(doc, dt, dn); @@ -33,9 +32,12 @@ $.extend(cur_frm.cscript, { }, production_item: function(doc) { - return this.frm.call({ - method: "get_item_details", - args: { item: doc.production_item } + frappe.call({ + method: "erpnext.manufacturing.doctype.production_order.production_order.get_item_details", + args: { item: doc.production_item }, + callback: function(r) { + cur_frm.set_value(r.message); + } }); }, @@ -53,6 +55,45 @@ $.extend(cur_frm.cscript, { frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } }); + }, + + bom_no: function() { + return this.frm.call({ + doc: this.frm.doc, + method: "set_production_order_operations", + callback: function(r) { + if(!r.exc) refresh_field("production_order_operations"); + } + }); + }, + + make_time_log: function(doc, cdt, cdn){ + var child = locals[cdt][cdn] + frappe.call({ + method:"erpnext.manufacturing.doctype.production_order.production_order.make_time_log", + args: { + "name": doc.name, + "operation": child.idx + ". " + child.operation, + "from_time": child.planned_start_time, + "to_time": child.planned_end_time, + "project": doc.project, + "workstation": child.workstation, + "qty": flt(doc.qty) - flt(child.completed_qty) + }, + callback: function(r) { + var doclist = frappe.model.sync(r.message); + 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 + } + }); } }); @@ -124,4 +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'); \ No newline at end of file +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 9ec5892638..9617ba0b6a 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -22,6 +22,7 @@ "reqd": 1 }, { + "default": "Draft", "depends_on": "eval:!doc.__islocal", "fieldname": "status", "fieldtype": "Select", @@ -51,7 +52,7 @@ "reqd": 1 }, { - "depends_on": "production_item", + "depends_on": "", "description": "Bill of Material to be considered for manufacturing", "fieldname": "bom_no", "fieldtype": "Link", @@ -64,14 +65,6 @@ "read_only": 0, "reqd": 1 }, - { - "default": "1", - "description": "If checked, BOM for sub-assembly items will be considered for getting raw materials. Otherwise, all sub-assembly items will be treated as a raw material.", - "fieldname": "use_multi_level_bom", - "fieldtype": "Check", - "label": "Use Multi-Level BOM", - "permlevel": 0 - }, { "fieldname": "column_break1", "fieldtype": "Column Break", @@ -81,16 +74,7 @@ "width": "50%" }, { - "description": "Manufacture against Sales Order", - "fieldname": "sales_order", - "fieldtype": "Link", - "label": "Sales Order", - "options": "Sales Order", - "permlevel": 0, - "read_only": 0 - }, - { - "depends_on": "production_item", + "depends_on": "", "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, @@ -102,13 +86,7 @@ "reqd": 1 }, { - "depends_on": "production_item", - "fieldname": "total_fixed_cost", - "fieldtype": "Float", - "label": "Total Fixed Cost", - "permlevel": 0 - }, - { + "default": "0", "depends_on": "eval:doc.docstatus==1", "description": "Automatically updated via Stock Entry of type Manufacture or Repack", "fieldname": "produced_qty", @@ -121,26 +99,64 @@ "read_only": 1 }, { - "depends_on": "sales_order", + "default": "1", + "description": "If checked, BOM for sub-assembly items will be considered for getting raw materials. Otherwise, all sub-assembly items will be treated as a raw material.", + "fieldname": "use_multi_level_bom", + "fieldtype": "Check", + "label": "Use Multi-Level BOM", + "permlevel": 0 + }, + { + "fieldname": "time", + "fieldtype": "Section Break", + "label": "Time", + "options": "icon-time", + "permlevel": 0, + "precision": "" + }, + { + "depends_on": "", "fieldname": "expected_delivery_date", "fieldtype": "Date", "label": "Expected Delivery Date", "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", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "actual_start_date", + "fieldtype": "Datetime", + "label": "Actual Start Date", + "permlevel": 0, + "precision": "", "read_only": 1 }, { - "fieldname": "start_date", + "fieldname": "actual_end_date", "fieldtype": "Datetime", - "label": "Start Date", + "label": "Actual End Date", "permlevel": 0, - "precision": "" - }, - { - "fieldname": "end_date", - "fieldtype": "Datetime", - "label": "End Date", - "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "fieldname": "warehouses", @@ -150,7 +166,7 @@ "permlevel": 0 }, { - "depends_on": "production_item", + "depends_on": "", "description": "Manufactured quantity will be updated in this warehouse", "fieldname": "fg_warehouse", "fieldtype": "Link", @@ -174,6 +190,86 @@ "permlevel": 0, "reqd": 0 }, + { + "depends_on": "", + "fieldname": "operations", + "fieldtype": "Section Break", + "label": "Operations", + "options": "icon-wrench", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "production_order_operations", + "fieldtype": "Table", + "label": "Production Order Operations", + "options": "Production Order Operation", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "section_break_22", + "fieldtype": "Section Break", + "label": "Operation Cost", + "options": "", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "planned_operating_cost", + "fieldtype": "Currency", + "label": "Planned Operating Cost", + "no_copy": 0, + "options": "Company:company:default_currency", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "actual_operating_cost", + "fieldtype": "Currency", + "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", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "total_operating_cost", + "fieldtype": "Currency", + "label": "Total 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", + "fieldname": "auto_time_log", + "fieldtype": "Button", + "label": "Automatically Make Time logs", + "permlevel": 0, + "precision": "" + }, { "fieldname": "more_info", "fieldtype": "Section Break", @@ -189,6 +285,24 @@ "permlevel": 0, "read_only": 1 }, + { + "depends_on": "", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "oldfieldname": "stock_uom", + "oldfieldtype": "Data", + "options": "UOM", + "permlevel": 0, + "read_only": 1 + }, + { + "fieldname": "column_break2", + "fieldtype": "Column Break", + "permlevel": 0, + "read_only": 0, + "width": "50%" + }, { "fieldname": "project_name", "fieldtype": "Link", @@ -201,22 +315,13 @@ "read_only": 0 }, { - "fieldname": "column_break2", - "fieldtype": "Column Break", - "permlevel": 0, - "read_only": 0, - "width": "50%" - }, - { - "depends_on": "production_item", - "fieldname": "stock_uom", + "description": "Manufacture against Sales Order", + "fieldname": "sales_order", "fieldtype": "Link", - "label": "Stock UOM", - "oldfieldname": "stock_uom", - "oldfieldtype": "Data", - "options": "UOM", + "label": "Sales Order", + "options": "Sales Order", "permlevel": 0, - "read_only": 1 + "read_only": 0 }, { "fieldname": "company", @@ -246,7 +351,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2014-10-27 13:42:31.476892", + "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 a68b3a1b10..2c2c04def2 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -4,15 +4,23 @@ from __future__ import unicode_literals import frappe, json -from frappe.utils import flt, nowdate +from frappe.utils import flt, nowdate, cstr, get_datetime, getdate from frappe import _ from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no +from dateutil.relativedelta import relativedelta 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 __setup__(self): + self.holidays = frappe._dict() + def validate(self): if self.docstatus == 0: self.status = "Draft" @@ -26,7 +34,7 @@ class ProductionOrder(Document): self.validate_sales_order() self.validate_warehouse() - self.set_fixed_cost() + self.calculate_operating_cost() from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) @@ -50,9 +58,16 @@ class ProductionOrder(Document): for w in [self.fg_warehouse, self.wip_warehouse]: validate_warehouse_company(w, self.company) - def set_fixed_cost(self): - if self.total_fixed_cost==None: - self.total_fixed_cost = frappe.db.get_value("BOM", self.bom_no, "total_fixed_cost") + def calculate_operating_cost(self): + 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 + + 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 @@ -145,6 +160,56 @@ class ProductionOrder(Document): from erpnext.stock.utils import update_bin update_bin(args) + def set_production_order_operations(self): + """Fetch operations from BOM and set in 'Production Order'""" + + self.set('production_order_operations', []) + + operations = frappe.db.sql("""select operation, opn_description, workstation, + 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.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) + + d.planned_start_time = scheduled_datetime + scheduled_datetime = get_datetime(scheduled_datetime) + relativedelta(minutes=d.time_in_mins) + d.planned_end_time = scheduled_datetime + + self.planned_end_date = scheduled_datetime + + + def get_holidays(self, workstation): + holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") + + if holiday_list not in self.holidays: + holiday_list_days = [getdate(d[0]) for d in frappe.get_all("Holiday", fields=["holiday_date"], + filters={"parent": holiday_list}, order_by="holiday_date", limit_page_length=0, as_list=1)] + + self.holidays[holiday_list] = holiday_list_days + + 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): res = frappe.db.sql("""select stock_uom, description @@ -155,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() @@ -170,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)) @@ -196,12 +259,46 @@ def get_events(start, end, filters=None): if filters[key]: conditions += " and " + key + ' = "' + filters[key].replace('"', '\"') + '"' - data = frappe.db.sql("""select name,production_item, start_date,end_date from `tabProduction Order` - where ((ifnull(start_date, '0000-00-00')!= '0000-00-00') \ - and (start_date between %(start)s and %(end)s) \ - or ((ifnull(start_date, '0000-00-00')!= '0000-00-00') \ - and end_date between %(start)s and %(end)s)){conditions}""".format(conditions=conditions), { + 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), { "start": start, "end": end - }, as_dict=True, update={"allDay": 0}) + }, as_dict=True, update={"allDay": 0}) return data + +@frappe.whitelist() +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 + time_log.to_time = to_time + time_log.production_order = name + 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, + flt(prod_order.qty) - flt(d.completed_qty), prod_order.project_name, d.workstation) + time_log.save() + 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/production_order_calendar.js b/erpnext/manufacturing/doctype/production_order/production_order_calendar.js index d4011a3fb3..3509d328e4 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order_calendar.js +++ b/erpnext/manufacturing/doctype/production_order/production_order_calendar.js @@ -3,8 +3,8 @@ frappe.views.calendar["Production Order"] = { field_map: { - "start": "start_date", - "end": "end_date", + "start": "production_start_date", + "end": "production_end_date", "id": "name", "title": "production_item", "allDay": "allDay" @@ -21,7 +21,7 @@ frappe.views.calendar["Production Order"] = { "fieldtype": "Link", "fieldname": "production_item", "options": "Item", - "label": __("Production Item") + "label": __("Production Item") }, { "fieldtype": "Link", @@ -31,4 +31,4 @@ frappe.views.calendar["Production Order"] = { } ], get_events_method: "erpnext.manufacturing.doctype.production_order.production_order.get_events" -} \ 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 96fe6e4dbc..49db178f45 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): @@ -58,4 +59,78 @@ class TestProductionOrder(unittest.TestCase): self.assertRaises(StockOverProductionError, s.submit) + def test_make_time_log(self): + prod_order = 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" + }) + + + 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", + "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) + + self.assertEqual(prod_order.name, time_log.production_order) + self.assertEqual((prod_order.qty - d.qty_completed), time_log.qty) + self.assertEqual(time_diff_in_hours(d.planned_end_time, d.planned_start_time),time_log.hours) + + time_log.save() + time_log.submit() + + manufacturing_settings = frappe.get_doc({ + "doctype": "Manufacturing Settings", + "maximum_overtime": 30, + "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) + + 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].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({ + "qty": 10, + "from_time": "2014-11-26 00:00:00", + "to_time": "2014-11-26 00:00:00", + "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/__init__.py b/erpnext/manufacturing/doctype/production_order_operation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json new file mode 100644 index 0000000000..f495b0bc19 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json @@ -0,0 +1,296 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2014-10-16 14:35:41.950175", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "fields": [ + { + "fieldname": "details", + "fieldtype": "Section Break", + "label": "Details", + "permlevel": 0, + "precision": "" + }, + { + "allow_on_submit": 0, + "fieldname": "operation", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Operation", + "no_copy": 0, + "oldfieldname": "operation_no", + "oldfieldtype": "Data", + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "opn_description", + "fieldtype": "Text", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Operation Description", + "no_copy": 0, + "oldfieldname": "opn_description", + "oldfieldtype": "Text", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "col_break1", + "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 + }, + { + "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": "", + "read_only": 1 + }, + { + "allow_on_submit": 0, + "fieldname": "workstation", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Workstation", + "no_copy": 0, + "oldfieldname": "workstation", + "oldfieldtype": "Link", + "options": "Workstation", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "fieldname": "estimated_time_and_cost", + "fieldtype": "Section Break", + "label": "Estimated Time and Cost", + "permlevel": 0, + "precision": "" + }, + { + "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": "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": 1, + "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 + }, + { + "allow_on_submit": 0, + "fieldname": "hour_rate", + "fieldtype": "Float", + "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": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "fieldname": "planned_operating_cost", + "fieldtype": "Currency", + "label": "Planned Operating Cost", + "no_copy": 0, + "options": "Company:company:default_currency", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Actual Time and Cost", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "actual_start_time", + "fieldtype": "Datetime", + "label": "Actual Start Time", + "no_copy": 1, + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "description": "Updated via 'Time Log'", + "fieldname": "actual_end_time", + "fieldtype": "Datetime", + "label": "Actual End Time", + "no_copy": 1, + "permlevel": 0, + "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\")", + "fieldname": "make_time_log", + "fieldtype": "Button", + "label": "Make Time Log", + "permlevel": 0, + "precision": "" + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "modified": "2014-12-23 15:42:34.892964", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Order Operation", + "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/production_order_operation/production_order_operation.py b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.py new file mode 100644 index 0000000000..b8f57d230e --- /dev/null +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.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 ProductionOrderOperation(Document): + pass diff --git a/erpnext/manufacturing/doctype/workstation/test_records.json b/erpnext/manufacturing/doctype/workstation/test_records.json index 72123eb282..37fb5aa4e4 100644 --- a/erpnext/manufacturing/doctype/workstation/test_records.json +++ b/erpnext/manufacturing/doctype/workstation/test_records.json @@ -4,7 +4,13 @@ "name": "_Test Workstation 1", "workstation_name": "_Test Workstation 1", "warehouse": "_Test warehouse - _TC", - "fixed_cycle_cost": 1000, - "hour_rate":100 + "hour_rate":100, + "holiday_list": "_Test Holiday List", + "workstation_operation_hours": [ + { + "start_time": "10:00:00", + "end_time": "20:00:00" + } + ] } ] diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 01746ebc44..24d2334a24 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -7,6 +7,9 @@ import unittest test_dependencies = ["Warehouse"] test_records = frappe.get_test_records('Workstation') - class TestWorkstation(unittest.TestCase): - pass + + 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/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index 6271a163cb..17320520af 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -1,13 +1,17 @@ // 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) { - + 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) { - -} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 6183fa3731..ecf0f667a5 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -1,161 +1,161 @@ { - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:workstation_name", - "creation": "2013-01-10 16:34:17", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Master", + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:workstation_name", + "creation": "2013-01-10 16:34:17", + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", "fields": [ { - "fieldname": "workstation_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Workstation Name", - "oldfieldname": "workstation_name", - "oldfieldtype": "Data", - "permlevel": 0, - "reqd": 1 - }, + "fieldname": "description_and_warehouse", + "fieldtype": "Section Break", + "label": "Description and Warehouse", + "permlevel": 0, + "precision": "" + }, { - "fieldname": "warehouse", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Warehouse", - "oldfieldname": "warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "permlevel": 0, + "fieldname": "workstation_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Workstation Name", + "oldfieldname": "workstation_name", + "oldfieldtype": "Data", + "permlevel": 0, "reqd": 1 - }, + }, { - "fieldname": "description", - "fieldtype": "Text", - "in_list_view": 1, - "label": "Description", - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, + "fieldname": "description", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "permlevel": 0, "width": "300px" - }, + }, { - "fieldname": "capacity", - "fieldtype": "Data", - "hidden": 1, - "in_list_view": 1, - "label": "Capacity", - "oldfieldname": "capacity", - "oldfieldtype": "Data", - "permlevel": 0, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse", + "oldfieldname": "warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "permlevel": 0, + "reqd": 1 + }, + { + "default": "", + "fieldname": "holiday_list", + "fieldtype": "Link", + "label": "Holiday List", + "options": "Holiday List", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "over_heads", + "fieldtype": "Section Break", + "label": "Operating Costs", + "oldfieldtype": "Section Break", + "permlevel": 0 + }, + { + "description": "per hour", + "fieldname": "hour_rate_electricity", + "fieldtype": "Float", + "label": "Electricity Cost", + "oldfieldname": "hour_rate_electricity", + "oldfieldtype": "Currency", + "permlevel": 0 + }, + { + "description": "per hour", + "fieldname": "hour_rate_consumable", + "fieldtype": "Float", + "label": "Consumable Cost", + "oldfieldname": "hour_rate_consumable", + "oldfieldtype": "Currency", + "permlevel": 0 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "description": "per hour", + "fieldname": "hour_rate_rent", + "fieldtype": "Float", + "label": "Rent Cost", + "oldfieldname": "hour_rate_rent", + "oldfieldtype": "Currency", + "permlevel": 0 + }, + { + "description": "Wages per hour", + "fieldname": "hour_rate_labour", + "fieldtype": "Float", + "label": "Wages", + "oldfieldname": "hour_rate_labour", + "oldfieldtype": "Currency", + "permlevel": 0, "reqd": 0 - }, + }, { - "fieldname": "capacity_units", - "fieldtype": "Select", - "hidden": 1, - "in_list_view": 1, - "label": "Capacity Units", - "oldfieldname": "capacity_units", - "oldfieldtype": "Select", - "options": "\nUnits/Shifts\nUnits/Hour", - "permlevel": 0, - "reqd": 0 - }, - { - "fieldname": "fixed_cycle_cost", - "fieldtype": "Float", - "label": "Fixed Cycle Cost", - "permlevel": 0 - }, - { - "fieldname": "hour_rate_labour", - "fieldtype": "Float", - "label": "Hour Rate Labour", - "oldfieldname": "hour_rate_labour", - "oldfieldtype": "Currency", - "permlevel": 0, - "reqd": 0 - }, - { - "fieldname": "over_heads", - "fieldtype": "Section Break", - "label": "Overheads", - "oldfieldtype": "Section Break", - "permlevel": 0 - }, - { - "description": "Electricity cost per hour", - "fieldname": "hour_rate_electricity", - "fieldtype": "Float", - "label": "Electricity Cost", - "oldfieldname": "hour_rate_electricity", - "oldfieldtype": "Currency", - "permlevel": 0 - }, - { - "description": "Consumable cost per hour", - "fieldname": "hour_rate_consumable", - "fieldtype": "Float", - "label": "Consumable Cost", - "oldfieldname": "hour_rate_consumable", - "oldfieldtype": "Currency", - "permlevel": 0 - }, - { - "description": "Rent per hour", - "fieldname": "hour_rate_rent", - "fieldtype": "Float", - "label": "Rent Cost", - "oldfieldname": "hour_rate_rent", - "oldfieldtype": "Currency", - "permlevel": 0 - }, - { - "fieldname": "overhead", - "fieldtype": "Float", - "label": "Overhead", - "oldfieldname": "overhead", - "oldfieldtype": "Currency", - "permlevel": 0, + "description": "per hour", + "fieldname": "hour_rate", + "fieldtype": "Float", + "label": "Net Hour Rate", + "oldfieldname": "hour_rate", + "oldfieldtype": "Currency", + "permlevel": 0, "read_only": 1 - }, + }, { - "fieldname": "hour_rate_section_break", - "fieldtype": "Section Break", - "label": "Hour Rate", - "oldfieldtype": "Section Break", - "permlevel": 0 - }, + "fieldname": "operation_timings", + "fieldtype": "Section Break", + "label": "Operation Timings", + "permlevel": 0, + "precision": "" + }, { - "fieldname": "hour_rate", - "fieldtype": "Float", - "label": "Hour Rate", - "oldfieldname": "hour_rate", - "oldfieldtype": "Currency", - "permlevel": 0, - "read_only": 1 + "fieldname": "workstation_operation_hours", + "fieldtype": "Table", + "label": "Workstation Operation Hours", + "options": "Workstation Operation Hour", + "permlevel": 0, + "precision": "" } - ], - "icon": "icon-wrench", - "idx": 1, - "modified": "2014-09-15 10:59:07.960814", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Workstation", - "owner": "Administrator", + ], + "icon": "icon-wrench", + "idx": 1, + "modified": "2014-12-23 15:27:58.477925", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation", + "owner": "Administrator", "permissions": [ { - "apply_user_permissions": 1, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "submit": 0, + "apply_user_permissions": 1, + "create": 1, + "delete": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "submit": 0, "write": 1 } ] -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index ec026c5c29..62819774c7 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -3,21 +3,77 @@ from __future__ import unicode_literals import frappe -from frappe.utils import flt +import datetime +from frappe import _ +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): - bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` + bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` where workstation = %s""", self.name) for bom_no in bom_list: - frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s - where parent = %s and workstation = %s""", + frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s + where parent = %s and workstation = %s""", (self.hour_rate, bom_no[0], self.name)) - + def on_update(self): - frappe.db.set(self, 'overhead', 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.overhead)) - self.update_bom_operation() \ No newline at end of file + self.validate_overlap_for_operation_timings() + + 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)) + + 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_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 not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")): + check_workstation_for_holiday(workstation, from_datetime, to_datetime) + +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 + + 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') + + 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") + 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_hour/__init__.py b/erpnext/manufacturing/doctype/workstation_operation_hour/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.json b/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.json new file mode 100644 index 0000000000..ebfa0ef7c5 --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.json @@ -0,0 +1,129 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2014-12-22 14:18:20.786493", + "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 + }, + { + "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": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "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 + }, + { + "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, + "hide_toolbar": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "modified": "2014-12-22 14:18:31.091653", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation Operation Hour", + "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_hour/workstation_operation_hour.py b/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.py new file mode 100644 index 0000000000..1b8c51960e --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operation_hour/workstation_operation_hour.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 WorkstationOperationHour(Document): + pass diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a81527ec79..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") @@ -98,3 +97,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/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 new file mode 100644 index 0000000000..291fc5c782 --- /dev/null +++ b/erpnext/patches/v5_0/capacity_planning.py @@ -0,0 +1,8 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + +def execute(): + 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/test_time_log.py b/erpnext/projects/doctype/time_log/test_time_log.py index 4a312adb41..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): @@ -16,6 +22,60 @@ class TestTimeLog(unittest.TestCase): self.assertRaises(OverlapError, ts.insert) 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.js b/erpnext/projects/doctype/time_log/time_log.js index d4d109d6f9..b23b3eadf4 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("completed_qty", is_manufacturing); }); // set to time if hours is updated @@ -26,4 +36,44 @@ 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) { + 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 = "\n" + operations.join("\n"); + refresh_field("operation"); + }) + } + }, + + operation: function(doc) { + return frappe.call({ + method: "erpnext.projects.doctype.time_log.time_log.get_workstation", + args: { + "production_order": doc.production_order, + "operation": doc.operation + }, + callback: function(r) { + if(!r.exc) { + console.log(r.message) + cur_frm.set_value("workstation", r.message) + } + } + }); + } +}); diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index 0eed1fc9af..343a697b10 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": "\nProject\nManufacturing", + "permlevel": 0, + "precision": "", + "reqd": 0 + }, { "fieldname": "from_time", "fieldtype": "Datetime", @@ -59,6 +68,7 @@ "reqd": 0 }, { + "depends_on": "eval:doc.time_log_for != 'Manufacturing'", "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 != 'Manufacturing'", "fieldname": "task", "fieldtype": "Link", "label": "Task", @@ -76,6 +87,42 @@ "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": "", + "read_only": 1 + }, + { + "description": "Operation completed for how many finished goods?", + "fieldname": "completed_qty", + "fieldtype": "Float", + "label": "Completed Qty", + "permlevel": 0, + "precision": "" + }, { "fieldname": "billable", "fieldtype": "Check", @@ -104,6 +151,7 @@ "read_only": 0 }, { + "depends_on": "eval:doc.time_log_for", "fieldname": "project", "fieldtype": "Link", "in_list_view": 1, @@ -151,7 +199,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2014-10-22 16:53:26.993828", + "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 6678392037..52257437b3 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -4,25 +4,37 @@ # 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, comma_and - +from frappe.utils import cstr, comma_and, flt class OverlapError(frappe.ValidationError): pass +class OverProductionError(frappe.ValidationError): pass +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.validate_time_log_for() + self.check_workstation_timings() + self.validate_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 on_submit(self): + self.update_production_order() + + def on_cancel(self): + self.update_production_order() + + def before_update_after_submit(self): + self.set_status() + + def before_cancel(self): + self.set_status() def set_status(self): self.status = { @@ -38,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 @@ -53,30 +66,111 @@ class TimeLog(Document): if existing: frappe.throw(_("This Time Log conflicts with {0}").format(comma_and(existing)), OverlapError) - def before_cancel(self): - self.set_status() + def validate_timings(self): + if self.to_time < self.from_time: + frappe.throw(_("From Time cannot be greater than To Time")) - def before_update_after_submit(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 validate_time_log_for(self): + if self.time_log_for == "Project": + for fld in ["production_order", "operation", "workstation", "completed_qty"]: + self.set(fld, None) + + def check_workstation_timings(self): + """Checks if **Time Log** is between operating hours of the **Workstation**.""" + if self.workstation: + 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_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 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() + + + frappe.db.sql("""update `tabProduction Order Operation` + 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, 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() + + 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_all_time_logs(self): + """Returns 'Actual Operating Time'. """ + 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_events(start, end): +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: + idx, operation = operation.split('. ',1) + + 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. + """ 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") + 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 from `tabTime Log` - where from_time between '%(start)s' and '%(end)s' or to_time between '%(start)s' and '%(end)s' - %(match)s""" % { + 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, - "match": match and (" and " + match) or "" - }, as_dict=True, update={"allDay": 0}) + "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 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/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"})) 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)) 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") %} +
{% } %}