diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 12d0babf20..7c1273d5e1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -17,7 +17,7 @@ "fieldname": "voucher_type", "fieldtype": "Select", "in_filter": 1, - "in_list_view": 1, + "in_list_view": 0, "label": "Voucher Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", @@ -289,7 +289,7 @@ }, { "default": "No", - "description": "Considered as Opening Balance", + "description": "", "fieldname": "is_opening", "fieldtype": "Select", "in_filter": 1, @@ -303,10 +303,10 @@ "search_index": 1 }, { - "description": "Actual Posting Date", + "description": "", "fieldname": "aging_date", "fieldtype": "Date", - "label": "Aging Date", + "label": "Ageing Date", "no_copy": 0, "oldfieldname": "aging_date", "oldfieldtype": "Date", @@ -355,6 +355,33 @@ "options": "Letter Head", "permlevel": 0 }, + { + "allow_on_submit": 1, + "fieldname": "select_print_heading", + "fieldtype": "Link", + "label": "Print Heading", + "no_copy": 1, + "oldfieldname": "select_print_heading", + "oldfieldtype": "Link", + "options": "Print Heading", + "permlevel": 0, + "print_hide": 1, + "read_only": 0, + "report_hide": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "oldfieldname": "amended_from", + "oldfieldtype": "Link", + "options": "Journal Entry", + "permlevel": 0, + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "column_break3", "fieldtype": "Column Break", @@ -427,37 +454,18 @@ "search_index": 1 }, { - "allow_on_submit": 1, - "fieldname": "select_print_heading", - "fieldtype": "Link", - "label": "Print Heading", - "no_copy": 1, - "oldfieldname": "select_print_heading", - "oldfieldtype": "Link", - "options": "Print Heading", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", "permlevel": 0, - "print_hide": 1, - "read_only": 0, - "report_hide": 1 - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Amended From", - "no_copy": 1, - "oldfieldname": "amended_from", - "oldfieldtype": "Link", - "options": "Journal Entry", - "permlevel": 0, - "print_hide": 1, - "read_only": 1 + "precision": "" } ], "icon": "icon-file-text", "idx": 1, "is_submittable": 1, - "modified": "2015-02-20 05:07:15.435166", + "modified": "2015-02-23 04:46:05.569476", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", @@ -514,5 +522,5 @@ "search_fields": "voucher_type,posting_date, due_date, cheque_no", "sort_field": "modified", "sort_order": "DESC", - "title_field": "voucher_type" + "title_field": "title" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 66e4854fa0..a0a5a86ec1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -40,6 +40,7 @@ class JournalEntry(AccountsController): self.validate_expense_claim() self.validate_credit_debit_note() self.validate_empty_accounts_table() + self.set_title() def on_submit(self): self.check_credit_limit() @@ -47,6 +48,9 @@ class JournalEntry(AccountsController): self.update_advance_paid() self.update_expense_claim() + def set_title(self): + self.title = self.pay_to_recd_from or self.accounts[0].account + def update_advance_paid(self): advance_paid = frappe._dict() for d in self.get("accounts"): @@ -442,16 +446,16 @@ class JournalEntry(AccountsController): if d.debit > pending_amount: frappe.throw(_("Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. \ Pending Amount is {2}".format(d.idx, d.against_expense_claim, pending_amount))) - + def validate_credit_debit_note(self): count = frappe.db.exists({ "doctype": "Journal Entry", "stock_entry":self.stock_entry, - "docstatus":1 + "docstatus":1 }) if count: frappe.throw(_("{0} already made against stock entry {1}".format(self.voucher_type, self.stock_entry))) - + def validate_empty_accounts_table(self): if not self.get('accounts'): frappe.throw("Accounts table cannot be blank.") @@ -462,11 +466,17 @@ def get_default_bank_cash_account(company, voucher_type, mode_of_payment=None): if mode_of_payment: account = get_bank_cash_account(mode_of_payment, company) if account.get("bank_cash_account"): - account.update({"balance": get_balance_on(account.get("cash_bank_account"))}) + account.update({"balance": get_balance_on(account.get("bank_cash_account"))}) return account - account = frappe.db.get_value("Company", company, \ - voucher_type=="Bank Voucher" and "default_bank_account" or "default_cash_account") + if voucher_type=="Bank Entry": + account = frappe.db.get_value("Company", company, "default_bank_account") + if not account: + account = frappe.db.get_value("Account", {"company": company, "account_type": "Bank"}) + elif voucher_type=="Cash Entry": + account = frappe.db.get_value("Company", company, "default_cash_account") + if not account: + account = frappe.db.get_value("Account", {"company": company, "account_type": "Cash"}) if account: return { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry_list.js b/erpnext/accounts/doctype/journal_entry/journal_entry_list.js index 101793cb6a..48d6115e3d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry_list.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry_list.js @@ -1,3 +1,12 @@ frappe.listview_settings['Journal Entry'] = { - add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"] + add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"], + get_indicator: function(doc) { + if(doc.docstatus==0) { + return [__("Draft", "red", "docstatus,=,0")] + } else if(doc.docstatus==2) { + return [__("Cancelled", "grey", "docstatus,=,2")] + } else { + return [__(doc.voucher_type), "blue", "voucher_type,=," + doc.voucher_type] + } + } }; diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index 6c915b7a81..bd258fbdcf 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -17,6 +17,11 @@ def get_data(): "name": "Production Order", "description": _("Orders released for production."), }, + { + "type": "doctype", + "name": "Time Log", + "description": _("Time Logs for manufacturing."), + }, { "type": "doctype", "name": "Item", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index be5d5fc350..1fa949d4db 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -34,11 +34,10 @@ }, { "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", + "description": "Try planning operations for X days in advance.", + "fieldname": "capacity_planning_for_days", + "fieldtype": "Data", + "label": "Capacity Planning For (Days)", "permlevel": 0, "precision": "" } @@ -51,7 +50,7 @@ "is_submittable": 0, "issingle": 1, "istable": 0, - "modified": "2015-02-05 05:11:41.192126", + "modified": "2015-02-23 09:05:58.927098", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index 1febb749d1..48fe7a0c2a 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -85,6 +85,14 @@ erpnext.production_order = { frm.add_custom_button(__('Unstop'), cur_frm.cscript['Unstop Production Order'], "icon-check", "btn-default"); } + + // opertions + if ((doc.operations || []).length) { + frm.add_custom_button(__('Show Time Logs'), function() { + frappe.route_options = {"production_order": frm.doc.name}; + frappe.set_route("List", "Time Log"); + }); + } } }, @@ -191,6 +199,11 @@ $.extend(cur_frm.cscript, { }); }, + show_time_logs: function(doc, doctype, name) { + frappe.route_options = {"operation_id": name}; + frappe.set_route("List", "Time Log"); + }, + make_time_log: function(doc, cdt, cdn){ var child = locals[cdt][cdn] frappe.call({ @@ -209,16 +222,7 @@ $.extend(cur_frm.cscript, { frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } }); - }, - - auto_time_log: function(doc){ - frappe.call({ - method:"erpnext.manufacturing.doctype.production_order.production_order.auto_make_time_log", - args: { - "production_order_id": doc.name - } - }); - }, + } }); cur_frm.cscript['Stop Production Order'] = function() { diff --git a/erpnext/manufacturing/doctype/production_order/production_order.json b/erpnext/manufacturing/doctype/production_order/production_order.json index ae66c5edd8..089fd38936 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -270,15 +270,6 @@ "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", @@ -360,7 +351,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2015-02-20 05:04:13.881343", + "modified": "2015-02-23 07:42:05.639225", "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 9226178d6f..607f9138d0 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -4,14 +4,19 @@ from __future__ import unicode_literals import frappe, json -from frappe.utils import flt, nowdate, cstr, get_datetime, getdate +from frappe.utils import flt, nowdate, get_datetime, getdate, date_diff, time_diff_in_seconds from frappe import _ from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from dateutil.relativedelta import relativedelta +from dateutil.parser import parse class OverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass +class OperationTooLongError(frappe.ValidationError): pass + +from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError, NotInWorkingHoursError +from erpnext.projects.doctype.time_log.time_log import OverlapError form_grid_templates = { "operations": "templates/form_grid/production_order_grid.html" @@ -141,6 +146,7 @@ class ProductionOrder(Document): if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) frappe.db.set(self,'status', 'Submitted') + self.make_time_logs() self.update_planned_qty(self.qty) @@ -204,6 +210,58 @@ class ProductionOrder(Document): return self.holidays[holiday_list] + def make_time_logs(self): + time_logs = [] + + plan_days = frappe.db.get_single_value("Manufacturing Settings", "capacity_planning_for_days") or 30 + + for d in self.operations: + time_log = make_time_log(self.name, d.operation, d.planned_start_time, d.planned_end_time, + flt(self.qty) - flt(d.completed_qty), self.project_name, d.workstation) + + self.check_operation_fits_in_working_hours(d) + + original_start_time = time_log.from_time + while True: + try: + time_log.save() + break + except WorkstationHolidayError: + time_log.move_to_next_day() + except NotInWorkingHoursError: + time_log.move_to_next_working_slot() + except OverlapError: + time_log.move_to_next_non_overlapping_slot() + + # reset end time + time_log.to_time = get_datetime(time_log.from_time) + relativedelta(minutes=d.time_in_mins) + + if date_diff(time_log.from_time, original_start_time) > plan_days: + frappe.msgprint(_("Unable to find Time Slot in the next {0} days for Operation {1}").format(plan_days, d.operation)) + break + + print time_log.as_json() + + if time_log.name: + time_logs.append(time_log.name) + + if time_logs: + frappe.msgprint(_("Time Logs created:") + "\n" + "\n".join(time_logs)) + + + def check_operation_fits_in_working_hours(self, d): + """Raises expection if operation is longer than working hours in the given workstation.""" + operation_length = time_diff_in_seconds(d.planned_end_time, d.planned_start_time) + + workstation = frappe.get_doc("Workstation", d.workstation) + for working_hour in workstation.working_hours: + slot_length = (parse(working_hour.end_time) - parse(working_hour.start_time)).total_seconds() + if slot_length >= operation_length: + return + + frappe.throw(_("Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations").format(d.operation, d.workstation), + OperationTooLongError) + def update_operation_status(self): for d in self.get("operations"): if not d.completed_qty: @@ -293,13 +351,14 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_time_log(name, operation, from_time, to_time, qty=None, project=None, workstation=None): +def make_time_log(name, operation, from_time, to_time, qty=None, project=None, workstation=None, operation_id=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_id = operation_id time_log.operation= operation time_log.workstation= workstation time_log.activity_type= "Manufacturing" @@ -307,20 +366,3 @@ def make_time_log(name, operation, from_time, to_time, qty=None, project=None, 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, "docstatus":1}): - 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.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_operation/production_order_operation.json b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json index 071e94115d..2fe6c32e7c 100644 --- a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json @@ -40,7 +40,7 @@ }, { "allow_on_submit": 0, - "fieldname": "opn_description", + "fieldname": "description", "fieldtype": "Text", "hidden": 0, "ignore_user_permissions": 0, @@ -124,6 +124,15 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 1, + "depends_on": "eval:(doc.docstatus==1 && doc.status!=\"Completed\")", + "fieldname": "show_time_logs", + "fieldtype": "Button", + "label": "Show Time Logs", + "permlevel": 0, + "precision": "" + }, { "fieldname": "estimated_time_and_cost", "fieldtype": "Section Break", @@ -266,7 +275,6 @@ "read_only": 1 }, { - "allow_on_submit": 1, "depends_on": "eval:(doc.docstatus==1 && doc.status!=\"Completed\")", "fieldname": "make_time_log", "fieldtype": "Button", @@ -282,7 +290,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-02-20 05:08:36.612612", + "modified": "2015-02-23 07:55:19.368919", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order Operation", diff --git a/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py b/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py index 6eddb3ff89..f365733d52 100644 --- a/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +++ b/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py @@ -8,6 +8,7 @@ from frappe.utils import cstr, flt, cint, nowdate, now, add_days, comma_and from frappe import msgprint, _ from frappe.model.document import Document +from erpnext.manufacturing.doctype.bom.bom import validate_bom_no class ProductionPlanningTool(Document): def __init__(self, arg1, arg2=None): @@ -156,20 +157,10 @@ class ProductionPlanningTool(Document): def validate_data(self): self.validate_company() for d in self.get('items'): - self.validate_bom_no(d) + validate_bom_no(d.item_code, d.bom_no) if not flt(d.planned_qty): frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx)) - def validate_bom_no(self, d): - if not d.bom_no: - frappe.throw(_("Please enter BOM for Item {0} at row {1}").format(d.item_code, d.idx)) - else: - bom = frappe.db.sql("""select name from `tabBOM` where name = %s and item = %s - and docstatus = 1 and is_active = 1""", - (d.bom_no, d.item_code), as_dict = 1) - if not bom: - frappe.throw(_("Incorrect or Inactive BOM {0} for Item {1} at row {2}").format(d.bom_no, d.item_code, d.idx)) - def raise_production_order(self): """It will raise production order (Draft) for all distinct FG items""" self.validate_data() @@ -218,7 +209,7 @@ class ProductionPlanningTool(Document): for key in items: pro = frappe.new_doc("Production Order") pro.update(items[key]) - + pro.planned_start_date = now() pro.set_production_order_operations() diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index e4cda3d9f8..b65560c14f 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -10,7 +10,7 @@ { "fieldname": "description_and_warehouse", "fieldtype": "Section Break", - "label": "Description and Warehouse", + "label": "", "permlevel": 0, "precision": "" }, @@ -70,7 +70,7 @@ { "description": "per hour", "fieldname": "hour_rate_electricity", - "fieldtype": "Float", + "fieldtype": "Currency", "label": "Electricity Cost", "oldfieldname": "hour_rate_electricity", "oldfieldtype": "Currency", @@ -79,7 +79,7 @@ { "description": "per hour", "fieldname": "hour_rate_consumable", - "fieldtype": "Float", + "fieldtype": "Currency", "label": "Consumable Cost", "oldfieldname": "hour_rate_consumable", "oldfieldtype": "Currency", @@ -94,7 +94,7 @@ { "description": "per hour", "fieldname": "hour_rate_rent", - "fieldtype": "Float", + "fieldtype": "Currency", "label": "Rent Cost", "oldfieldname": "hour_rate_rent", "oldfieldtype": "Currency", @@ -103,7 +103,7 @@ { "description": "Wages per hour", "fieldname": "hour_rate_labour", - "fieldtype": "Float", + "fieldtype": "Currency", "label": "Wages", "oldfieldname": "hour_rate_labour", "oldfieldtype": "Currency", @@ -113,7 +113,7 @@ { "description": "per hour", "fieldname": "hour_rate", - "fieldtype": "Float", + "fieldtype": "Currency", "label": "Net Hour Rate", "oldfieldname": "hour_rate", "oldfieldtype": "Currency", @@ -123,7 +123,7 @@ { "fieldname": "working_hours_section", "fieldtype": "Section Break", - "label": "Wroking Hours", + "label": "Working Hours", "permlevel": 0, "precision": "" }, @@ -133,12 +133,13 @@ "label": "Working Hours", "options": "Workstation Working Hour", "permlevel": 0, - "precision": "" + "precision": "", + "reqd": 1 } ], "icon": "icon-wrench", "idx": 1, - "modified": "2015-02-05 05:13:38.580439", + "modified": "2015-02-23 09:43:35.903827", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 00f7ee644b..d345e263a7 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -10,7 +10,7 @@ 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 NotInWorkingHoursError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass class Workstation(Document): @@ -31,6 +31,7 @@ class Workstation(Document): self.update_bom_operation() def validate_overlap_for_operation_timings(self): + """Check if there is no overlap in setting Workstation Operating Hours""" for d in self.get("working_hours"): existing = frappe.db.sql_list("""select idx from `tabWorkstation Working Hour` where parent = %s and name != %s @@ -49,7 +50,7 @@ def get_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) + frappe.throw(_("Time Log timings outside workstation operating hours"), NotInWorkingHoursError) if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")): check_workstation_for_holiday(workstation, from_datetime, to_datetime) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c3e5dfa9a1..05b92b1684 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -123,3 +123,4 @@ erpnext.patches.v5_0.stock_entry_update_value erpnext.patches.v5_0.convert_stock_reconciliation erpnext.patches.v5_0.update_projects erpnext.patches.v5_0.item_patches +erpnext.patches.v5_0.update_journal_entry_title diff --git a/erpnext/patches/v5_0/update_journal_entry_title.py b/erpnext/patches/v5_0/update_journal_entry_title.py new file mode 100644 index 0000000000..59e6f41bd2 --- /dev/null +++ b/erpnext/patches/v5_0/update_journal_entry_title.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + frappe.db.sql("""update `tabJournal Entry` set title = + if(ifnull(pay_to_recd_from, "")!="", pay_to_recd_from, + (select account from `tabJournal Entry Account` + where parent=`tabJournal Entry`.name and idx=1 limit 1))""") diff --git a/erpnext/projects/doctype/project/project_list.js b/erpnext/projects/doctype/project/project_list.js index e440de8a20..8281c7dbb9 100644 --- a/erpnext/projects/doctype/project/project_list.js +++ b/erpnext/projects/doctype/project/project_list.js @@ -1,6 +1,5 @@ frappe.listview_settings['Project'] = { - add_fields: ["status", "priority", "is_active", "percent_complete", - "percent_milestones_completed", "completion_date"], + add_fields: ["status", "priority", "is_active", "percent_complete", "completion_date"], filters:[["status","=", "Open"]], get_indicator: function(doc) { if(doc.status=="Open" && doc.percent_complete) { diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index 8172650c8e..5f7e84615f 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -10,6 +10,14 @@ frappe.ui.form.on("Time Log", "onload", function(frm) { } }); +frappe.ui.form.on("Time Log", "refresh", function(frm) { + // set default user if created + if (frm.doc.__islocal && !frm.doc.user) { + frm.set_value("user", user); + } +}); + + // set to time if hours is updated frappe.ui.form.on("Time Log", "hours", function(frm) { if(!frm.doc.from_time) { diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index bc68805580..603efe36f0 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -50,6 +50,14 @@ "permlevel": 0, "read_only": 0 }, + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "permlevel": 0, + "precision": "" + }, { "fieldname": "column_break_3", "fieldtype": "Column Break", @@ -120,6 +128,15 @@ "permlevel": 0, "precision": "" }, + { + "fieldname": "operation_id", + "fieldtype": "Data", + "label": "Operation ID", + "options": "", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, { "fieldname": "column_break_14", "fieldtype": "Column Break", @@ -213,7 +230,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2015-02-19 04:16:33.756377", + "modified": "2015-02-23 08:00:48.195775", "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 33855afb53..3adf8793a2 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -6,7 +6,9 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -from frappe.utils import cstr, comma_and, flt +from frappe.utils import cstr, flt, add_days, get_datetime, get_time +from dateutil.relativedelta import relativedelta +from dateutil.parser import parse class OverlapError(frappe.ValidationError): pass class OverProductionError(frappe.ValidationError): pass @@ -52,24 +54,35 @@ 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 + """Checks if 'Time Log' entries overlap for a user, workstation. """ + self.validate_overlap_for("user") + self.validate_overlap_for("workstation") + + def validate_overlap_for(self, fieldname): + existing = self.get_overlap_for(fieldname) + if existing: + frappe.throw(_("This Time Log conflicts with {0} for {1}").format(existing.name, + self.meta.get_label(fieldname)), OverlapError) + + def get_overlap_for(self, fieldname): + if not self.get(fieldname): + return + existing = frappe.db.sql("""select name, from_time, to_time from `tabTime Log` where `{0}`=%s and ( (from_time between %s and %s) or (to_time between %s and %s) or (%s between from_time and to_time)) and name!=%s and ifnull(task, "")=%s - and docstatus < 2""", - (self.owner, self.from_time, self.to_time, self.from_time, + and docstatus < 2""".format(fieldname), + (self.get(fieldname), self.from_time, self.to_time, self.from_time, self.to_time, self.from_time, self.name or "No Name", - cstr(self.task))) + cstr(self.task)), as_dict=True) - if existing: - frappe.throw(_("This Time Log conflicts with {0}").format(comma_and(existing)), OverlapError) + return existing[0] if existing else None def validate_timings(self): - if self.to_time < self.from_time: + if get_datetime(self.to_time) < get_datetime(self.from_time): frappe.throw(_("From Time cannot be greater than To Time")) def calculate_total_hours(self): @@ -97,7 +110,7 @@ class TimeLog(Document): """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) + operation = self.operation dates = self.get_operation_start_end_time() tl = self.get_all_time_logs() @@ -122,6 +135,31 @@ class TimeLog(Document): where production_order = %s and operation = %s and docstatus=1""", (self.production_order, self.operation), as_dict=1)[0] + def move_to_next_day(self): + """Move start and end time one day forward""" + self.from_time = add_days(self.from_time, 1) + + def move_to_next_working_slot(self): + """Move to next working slot from workstation""" + workstation = frappe.get_doc("Workstation", self.workstation) + slot_found = False + for working_hour in workstation.working_hours: + if get_datetime(self.from_time).time() < get_time(working_hour.start_time): + self.from_time = self.from_time.split()[0] + " " + working_hour.start_time + slot_found = True + break + + if not slot_found: + # later than last time + self.from_time = self.from_time.split()[0] + workstation.working_hours[0].start_time + self.move_to_next_day() + + def move_to_next_non_overlapping_slot(self): + """If in overlap, set start as the end point of the overlapping time log""" + overlapping = self.get_overlap_for("workstation") + if overlapping: + self.from_time = parse(overlapping.to_time) + relativedelta(minutes=10) + def get_all_time_logs(self): """Returns 'Actual Operating Time'. """ return frappe.db.sql("""select diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 9083ead873..83f7a84e9f 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -166,6 +166,7 @@ "search_index": 0 }, { + "depends_on": "eval:!!!doc.variant_of", "fieldname": "variants_section", "fieldtype": "Section Break", "label": "Variants", @@ -702,7 +703,7 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Default BOM", - "no_copy": 1, + "no_copy": 0, "oldfieldname": "default_bom", "oldfieldtype": "Link", "options": "BOM", @@ -874,7 +875,7 @@ "icon": "icon-tag", "idx": 1, "max_attachments": 1, - "modified": "2015-02-22 23:59:37.956424", + "modified": "2015-02-23 06:12:13.359646", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index aa338ec5d0..5eb6a9bd36 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -187,6 +187,7 @@ class Item(WebsiteGenerator): frappe.msgprint(_("Item Variants {0} deleted").format(", ".join(deleted))) def get_variant_item_codes(self): + """Get all possible suffixes for variants""" if not self.variants: return [] @@ -234,7 +235,7 @@ class Item(WebsiteGenerator): from frappe.model import no_value_fields for field in self.meta.fields: if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\ - and field.fieldname != "item_code": + and field.fieldname not in ("item_code", "item_name"): if variant.get(field.fieldname) != template.get(field.fieldname): variant.set(field.fieldname, template.get(field.fieldname)) variant.__dirty = True @@ -245,7 +246,10 @@ class Item(WebsiteGenerator): template.get_variant_item_codes() for attr in template.variant_attributes[variant.item_code]: - variant.description += "\n" + attr[0] + ": " + attr[1] + variant.description += "

" + attr[0] + ": " + attr[1] + "

" + + variant.item_name = self.item_name + variant.item_code[len(self.name):] + variant.variant_of = template.name variant.has_variants = 0 variant.show_in_website = 0 @@ -292,7 +296,7 @@ class Item(WebsiteGenerator): if self.default_bom: bom_item = frappe.db.get_value("BOM", self.default_bom, "item") if bom_item not in (self.name, self.variant_of): - frappe.throw(_("Default BOM must be for this item or its template")) + frappe.throw(_("Default BOM ({0}) must be active for this item or its template").format(bom_item)) if self.is_purchase_item != "Yes": bom_mat = frappe.db.sql("""select distinct t1.parent diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 54e7192728..4e229cbaec 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -179,13 +179,13 @@ class StockEntry(StockController): self.production_order = None def check_if_operations_completed(self): + """Check if Time Logs are completed against before manufacturing to capture operating costs.""" prod_order = frappe.get_doc("Production Order", self.production_order) - if prod_order.actual_operating_cost: - for d in prod_order.get("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)) + for d in prod_order.get("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", { diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 29897ef072..d7ace2fe1d 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -124,6 +124,9 @@ def validate_item_details(args, item): elif item.is_sales_item != "Yes": throw(_("Item {0} must be a Sales Item").format(item.name)) + if cint(item.has_variants): + throw(_("Item {0} is a template, please select one of its variants").format(item.name)) + elif args.transaction_type == "buying" and args.parenttype != "Material Request": # validate if purchase item or subcontracted item if item.is_purchase_item != "Yes":