[capacity planning] automatically plan Time Logs from Production Order
This commit is contained in:
parent
93b2f167f8
commit
a1da88a3d3
@ -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"
|
||||
}
|
@ -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 {
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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() {
|
||||
|
@ -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",
|
||||
|
@ -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))
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
7
erpnext/patches/v5_0/update_journal_entry_title.py
Normal file
7
erpnext/patches/v5_0/update_journal_entry_title.py
Normal file
@ -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))""")
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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 += "<p>" + attr[0] + ": " + attr[1] + "</p>"
|
||||
|
||||
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
|
||||
|
@ -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", {
|
||||
|
@ -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":
|
||||
|
Loading…
Reference in New Issue
Block a user