diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 45b25f6a4cd..534489c9729 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '10.1.51' +__version__ = '10.1.52' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.js b/erpnext/hr/doctype/salary_slip/salary_slip.js index cbbc9e96aa3..0d22c44417e 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.js +++ b/erpnext/hr/doctype/salary_slip/salary_slip.js @@ -122,14 +122,20 @@ frappe.ui.form.on('Salary Slip Timesheet', { // Get leave details //--------------------------------------------------------------------- var get_emp_and_leave_details = function(doc, dt, dn) { - return frappe.call({ - method: 'get_emp_and_leave_details', - doc: locals[dt][dn], - callback: function(r, rt) { - cur_frm.refresh(); - calculate_all(doc, dt, dn); - } - }); + if(!doc.start_date){ + return frappe.call({ + method: 'get_emp_and_leave_details', + doc: locals[dt][dn], + callback: function(r, rt) { + cur_frm.refresh(); + calculate_all(doc, dt, dn); + } + }); + } +} + +cur_frm.cscript.employee = function(doc,dt,dn){ + get_emp_and_leave_details(doc, dt, dn); } cur_frm.cscript.leave_without_pay = function(doc,dt,dn){ diff --git a/erpnext/hub_node/data_migration_mapping/__init__.py b/erpnext/hub_node/data_migration_mapping/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hub_node/data_migration_mapping/company_to_hub_company/__init__.py b/erpnext/hub_node/data_migration_mapping/company_to_hub_company/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/__init__.py b/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/__init__.py deleted file mode 100644 index 0ea1bc4b1a0..00000000000 --- a/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -import frappe, json - -def pre_process(doc): - return json.loads(doc['data']) - -def post_process(remote_doc=None, local_doc=None, **kwargs): - if not local_doc: - return - - hub_message = remote_doc - # update hub message on hub - hub_connector = frappe.get_doc('Data Migration Connector', 'Hub Connector') - connection = hub_connector.get_connection() - connection.update('Hub Message', dict( - status='Synced' - ), hub_message['name']) - - # make opportunity after lead is created - lead = local_doc - opportunity = frappe.get_doc({ - 'doctype': 'Opportunity', - 'naming_series': 'OPTY-', - 'opportunity_type': 'Hub', - 'enquiry_from': 'Lead', - 'status': 'Open', - 'lead': lead.name, - 'company': lead.company, - 'transaction_date': frappe.utils.today() - }).insert() diff --git a/erpnext/hub_node/data_migration_mapping/item_to_hub_item/__init__.py b/erpnext/hub_node/data_migration_mapping/item_to_hub_item/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/hub_node/doctype/__init__.py b/erpnext/hub_node/doctype/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 04f9717c08b..c91bb8f3324 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -62,7 +62,7 @@ def enqueue_replace_bom(args): if isinstance(args, string_types): args = json.loads(args) - frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", args=args) + frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", args=args, timeout=4000) frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py new file mode 100644 index 00000000000..2f2c40ef2d4 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -0,0 +1,646 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate +from frappe.model.document import Document +from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict +from dateutil.relativedelta import relativedelta +from erpnext.stock.doctype.item.item import validate_end_of_life +from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError +from erpnext.projects.doctype.timesheet.timesheet import OverlapError +from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs +from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations +from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty +from frappe.utils.csvutils import getlink +from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty +from erpnext.utilities.transaction_base import validate_uom_is_integer + +class OverProductionError(frappe.ValidationError): pass +class StockOverProductionError(frappe.ValidationError): pass +class OperationTooLongError(frappe.ValidationError): pass +class ItemHasVariantError(frappe.ValidationError): pass + +form_grid_templates = { + "operations": "templates/form_grid/production_order_grid.html" +} + +class ProductionOrder(Document): + def validate(self): + self.validate_production_item() + if self.bom_no: + validate_bom_no(self.production_item, self.bom_no) + + self.validate_sales_order() + self.set_default_warehouse() + self.validate_warehouse_belongs_to_company() + self.calculate_operating_cost() + self.validate_qty() + self.validate_operation_time() + self.status = self.get_status() + + validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) + + self.set_required_items(reset_only_qty = len(self.get("required_items"))) + + def validate_sales_order(self): + if self.sales_order: + so = frappe.db.sql(""" + select so.name, so_item.delivery_date, so.project + from `tabSales Order` so + inner join `tabSales Order Item` so_item on so_item.parent = so.name + left join `tabProduct Bundle Item` pk_item on so_item.item_code = pk_item.parent + where so.name=%s and so.docstatus = 1 and ( + so_item.item_code=%s or + pk_item.item_code=%s ) + """, (self.sales_order, self.production_item, self.production_item), as_dict=1) + + if not so: + so = frappe.db.sql(""" + select + so.name, so_item.delivery_date, so.project + from + `tabSales Order` so, `tabSales Order Item` so_item, `tabPacked Item` packed_item + where so.name=%s + and so.name=so_item.parent + and so.name=packed_item.parent + and so_item.item_code = packed_item.parent_item + and so.docstatus = 1 and packed_item.item_code=%s + """, (self.sales_order, self.production_item), as_dict=1) + + if len(so): + if not self.expected_delivery_date: + self.expected_delivery_date = so[0].delivery_date + + if so[0].project: + self.project = so[0].project + + if not self.material_request: + self.validate_production_order_against_so() + else: + frappe.throw(_("Sales Order {0} is not valid").format(self.sales_order)) + + def set_default_warehouse(self): + if not self.wip_warehouse: + self.wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") + if not self.fg_warehouse: + self.fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_fg_warehouse") + + def validate_warehouse_belongs_to_company(self): + warehouses = [self.fg_warehouse, self.wip_warehouse] + for d in self.get("required_items"): + if d.source_warehouse not in warehouses: + warehouses.append(d.source_warehouse) + + for wh in warehouses: + validate_warehouse_company(wh, self.company) + + def calculate_operating_cost(self): + self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0 + for d in self.get("operations"): + d.planned_operating_cost = flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0) + d.actual_operating_cost = flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0) + + 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 + ordered_qty_against_so = frappe.db.sql("""select sum(qty) from `tabProduction Order` + where production_item = %s and sales_order = %s and docstatus < 2 and name != %s""", + (self.production_item, self.sales_order, self.name))[0][0] + + total_qty = flt(ordered_qty_against_so) + flt(self.qty) + + # get qty from Sales Order Item table + so_item_qty = frappe.db.sql("""select sum(stock_qty) from `tabSales Order Item` + where parent = %s and item_code = %s""", + (self.sales_order, self.production_item))[0][0] + # get qty from Packing Item table + dnpi_qty = frappe.db.sql("""select sum(qty) from `tabPacked Item` + where parent = %s and parenttype = 'Sales Order' and item_code = %s""", + (self.sales_order, self.production_item))[0][0] + # total qty in SO + so_qty = flt(so_item_qty) + flt(dnpi_qty) + + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", + "over_production_allowance_percentage")) + + if total_qty > so_qty + (allowance_percentage/100 * so_qty): + frappe.throw(_("Cannot produce more Item {0} than Sales Order quantity {1}") + .format(self.production_item, so_qty), OverProductionError) + + def update_status(self, status=None): + '''Update status of production order if unknown''' + if status != "Stopped": + status = self.get_status(status) + + if status != self.status: + self.db_set("status", status) + + self.update_required_items() + + return status + + def get_status(self, status=None): + '''Return the status based on stock entries against this production order''' + if not status: + status = self.status + + if self.docstatus==0: + status = 'Draft' + elif self.docstatus==1: + if status != 'Stopped': + stock_entries = frappe._dict(frappe.db.sql("""select purpose, sum(fg_completed_qty) + from `tabStock Entry` where production_order=%s and docstatus=1 + group by purpose""", self.name)) + + status = "Not Started" + if stock_entries: + status = "In Process" + produced_qty = stock_entries.get("Manufacture") + if flt(produced_qty) == flt(self.qty): + status = "Completed" + else: + status = 'Cancelled' + + return status + + def update_production_order_qty(self): + """Update **Manufactured Qty** and **Material Transferred for Qty** in Production Order + based on Stock Entry""" + + for purpose, fieldname in (("Manufacture", "produced_qty"), + ("Material Transfer for Manufacture", "material_transferred_for_manufacturing")): + qty = flt(frappe.db.sql("""select sum(fg_completed_qty) + from `tabStock Entry` where production_order=%s and docstatus=1 + and purpose=%s""", (self.name, purpose))[0][0]) + + if qty > self.qty: + frappe.throw(_("{0} ({1}) cannot be greater than planned quanitity ({2}) in Production Order {3}").format(\ + self.meta.get_label(fieldname), qty, self.qty, self.name), StockOverProductionError) + + self.db_set(fieldname, qty) + + def before_submit(self): + self.make_time_logs() + + def on_submit(self): + if not self.wip_warehouse: + frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) + if not self.fg_warehouse: + frappe.throw(_("For Warehouse is required before Submit")) + + self.update_reserved_qty_for_production() + self.update_completed_qty_in_material_request() + self.update_planned_qty() + + def on_cancel(self): + self.validate_cancel() + + frappe.db.set(self,'status', 'Cancelled') + self.delete_timesheet() + self.update_completed_qty_in_material_request() + self.update_planned_qty() + self.update_reserved_qty_for_production() + + def validate_cancel(self): + if self.status == "Stopped": + frappe.throw(_("Stopped Production Order cannot be cancelled, Unstop it first to cancel")) + + # Check whether any stock entry exists against this Production Order + stock_entry = frappe.db.sql("""select name from `tabStock Entry` + where production_order = %s and docstatus = 1""", self.name) + if stock_entry: + frappe.throw(_("Cannot cancel because submitted Stock Entry {0} exists").format(stock_entry[0][0])) + + def update_planned_qty(self): + update_bin_qty(self.production_item, self.fg_warehouse, { + "planned_qty": get_planned_qty(self.production_item, self.fg_warehouse) + }) + + if self.material_request: + mr_obj = frappe.get_doc("Material Request", self.material_request) + mr_obj.update_requested_qty([self.material_request_item]) + + def update_completed_qty_in_material_request(self): + if self.material_request: + frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) + + def set_production_order_operations(self): + """Fetch operations from BOM and set in 'Production Order'""" + self.set('operations', []) + + if not self.bom_no \ + or cint(frappe.db.get_single_value("Manufacturing Settings", "disable_capacity_planning")): + return + + if self.use_multi_level_bom: + bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() + else: + bom_list = [self.bom_no] + + operations = frappe.db.sql(""" + select + operation, description, workstation, idx, + base_hour_rate as hour_rate, time_in_mins, + "Pending" as status, parent as bom + from + `tabBOM Operation` + where + parent in (%s) order by idx + """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1) + + self.set('operations', operations) + self.calculate_time() + + def calculate_time(self): + bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + + for d in self.get("operations"): + d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * flt(self.qty) + + self.calculate_operating_cost() + + def get_holidays(self, workstation): + holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") + + holidays = {} + + if holiday_list not in 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)] + + holidays[holiday_list] = holiday_list_days + + return holidays[holiday_list] + + def make_time_logs(self, open_new=False): + """Capacity Planning. Plan time logs based on earliest availablity of workstation after + Planned Start Date. Time logs will be created and remain in Draft mode and must be submitted + before manufacturing entry can be made.""" + + if not self.operations: + return + + timesheets = [] + plan_days = frappe.db.get_single_value("Manufacturing Settings", "capacity_planning_for_days") or 30 + + timesheet = make_timesheet(self.name, self.company) + timesheet.set('time_logs', []) + + for i, d in enumerate(self.operations): + + if d.status != 'Completed': + self.set_start_end_time_for_workstation(d, i) + + args = self.get_operations_data(d) + + add_timesheet_detail(timesheet, args) + original_start_time = d.planned_start_time + + # validate operating hours if workstation [not mandatory] is specified + try: + timesheet.validate_time_logs() + except OverlapError: + if frappe.message_log: frappe.message_log.pop() + timesheet.schedule_for_production_order(d.idx) + except WorkstationHolidayError: + if frappe.message_log: frappe.message_log.pop() + timesheet.schedule_for_production_order(d.idx) + + from_time, to_time = self.get_start_end_time(timesheet, d.name) + + if date_diff(from_time, original_start_time) > cint(plan_days): + frappe.throw(_("Unable to find Time Slot in the next {0} days for Operation {1}").format(plan_days, d.operation)) + break + + d.planned_start_time = from_time + d.planned_end_time = to_time + d.db_update() + + if timesheet and open_new: + return timesheet + + if timesheet and timesheet.get("time_logs"): + timesheet.save() + timesheets.append(getlink("Timesheet", timesheet.name)) + + self.planned_end_date = self.operations[-1].planned_end_time + if timesheets: + frappe.local.message_log = [] + frappe.msgprint(_("Timesheet created:") + "\n" + "\n".join(timesheets)) + + def get_operations_data(self, data): + return { + 'from_time': get_datetime(data.planned_start_time), + 'hours': data.time_in_mins / 60.0, + 'to_time': get_datetime(data.planned_end_time), + 'project': self.project, + 'operation': data.operation, + 'operation_id': data.name, + 'workstation': data.workstation, + 'completed_qty': flt(self.qty) - flt(data.completed_qty) + } + + def set_start_end_time_for_workstation(self, data, index): + """Set start and end time for given operation. If first operation, set start as + `planned_start_date`, else add time diff to end time of earlier operation.""" + + if index == 0: + data.planned_start_time = self.planned_start_date + else: + data.planned_start_time = get_datetime(self.operations[index-1].planned_end_time)\ + + get_mins_between_operations() + + data.planned_end_time = get_datetime(data.planned_start_time) + relativedelta(minutes = data.time_in_mins) + + if data.planned_start_time == data.planned_end_time: + frappe.throw(_("Capacity Planning Error")) + + def get_start_end_time(self, timesheet, operation_id): + for data in timesheet.time_logs: + if data.operation_id == operation_id: + return data.from_time, data.to_time + + def check_operation_fits_in_working_hours(self, d): + """Raises expection if operation is longer than working hours in the given workstation.""" + from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours + check_if_within_operating_hours(d.workstation, d.operation, d.planned_start_time, d.planned_end_time) + + def update_operation_status(self): + for d in self.get("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'")) + + def set_actual_dates(self): + self.actual_start_date = None + self.actual_end_date = None + if self.get("operations"): + actual_start_dates = [d.actual_start_time for d in self.get("operations") if d.actual_start_time] + if actual_start_dates: + self.actual_start_date = min(actual_start_dates) + + actual_end_dates = [d.actual_end_time for d in self.get("operations") if d.actual_end_time] + if actual_end_dates: + self.actual_end_date = max(actual_end_dates) + + def delete_timesheet(self): + for timesheet in frappe.get_all("Timesheet", ["name"], {"production_order": self.name}): + frappe.delete_doc("Timesheet", timesheet.name) + + def validate_production_item(self): + if frappe.db.get_value("Item", self.production_item, "has_variants"): + frappe.throw(_("Production Order cannot be raised against a Item Template"), ItemHasVariantError) + + if self.production_item: + validate_end_of_life(self.production_item) + + def validate_qty(self): + if not self.qty > 0: + frappe.throw(_("Quantity to Manufacture must be greater than 0.")) + + def validate_operation_time(self): + for d in self.operations: + if not d.time_in_mins > 0: + frappe.throw(_("Operation Time must be greater than 0 for Operation {0}".format(d.operation))) + + def update_required_items(self): + ''' + update bin reserved_qty_for_production + called from Stock Entry for production, after submit, cancel + ''' + if self.docstatus==1: + # calculate transferred qty based on submitted stock entries + self.update_transaferred_qty_for_required_items() + + # update in bin + self.update_reserved_qty_for_production() + + def update_reserved_qty_for_production(self, items=None): + '''update reserved_qty_for_production in bins''' + for d in self.required_items: + if d.source_warehouse: + stock_bin = get_bin(d.item_code, d.source_warehouse) + stock_bin.update_reserved_qty_for_production() + + def get_items_and_operations_from_bom(self): + self.set_required_items() + self.set_production_order_operations() + + return check_if_scrap_warehouse_mandatory(self.bom_no) + + def set_available_qty(self): + for d in self.get("required_items"): + if d.source_warehouse: + d.available_qty_at_source_warehouse = get_latest_stock_qty(d.item_code, d.source_warehouse) + + if self.wip_warehouse: + d.available_qty_at_wip_warehouse = get_latest_stock_qty(d.item_code, self.wip_warehouse) + + def set_required_items(self, reset_only_qty=False): + '''set required_items for production to keep track of reserved qty''' + if not reset_only_qty: + self.required_items = [] + + if self.bom_no and self.qty: + item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=self.qty, + fetch_exploded = self.use_multi_level_bom) + + if reset_only_qty: + for d in self.get("required_items"): + if item_dict.get(d.item_code): + d.required_qty = item_dict.get(d.item_code).get("qty") + else: + for item in sorted(item_dict.values(), key=lambda d: d['idx']): + self.append('required_items', { + 'item_code': item.item_code, + 'item_name': item.item_name, + 'description': item.description, + 'required_qty': item.qty, + 'source_warehouse': item.source_warehouse or item.default_warehouse + }) + + self.set_available_qty() + + def update_transaferred_qty_for_required_items(self): + '''update transferred qty from submitted stock entries for that item against + the production order''' + + for d in self.required_items: + transferred_qty = frappe.db.sql('''select sum(qty) + from `tabStock Entry` entry, `tabStock Entry Detail` detail + where + entry.production_order = %s + and entry.purpose = "Material Transfer for Manufacture" + and entry.docstatus = 1 + and detail.parent = entry.name + and detail.item_code = %s''', (self.name, d.item_code))[0][0] + + d.db_set('transferred_qty', flt(transferred_qty), update_modified = False) + + +@frappe.whitelist() +def get_item_details(item, project = None): + res = frappe.db.sql(""" + select stock_uom, description + from `tabItem` + where disabled=0 + and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %s) + and name=%s + """, (nowdate(), item), as_dict=1) + + if not res: + return {} + + res = res[0] + + filters = {"item": item, "is_default": 1} + + if project: + filters = {"item": item, "project": project} + + res["bom_no"] = frappe.db.get_value("BOM", filters = filters) + + if not res["bom_no"]: + variant_of= frappe.db.get_value("Item", item, "variant_of") + + if variant_of: + res["bom_no"] = frappe.db.get_value("BOM", filters={"item": variant_of, "is_default": 1}) + + if not res["bom_no"]: + if project: + res = get_item_details(item) + frappe.msgprint(_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1) + else: + frappe.throw(_("Default BOM for {0} not found").format(item)) + + res['project'] = project or frappe.db.get_value('BOM', res['bom_no'], 'project') + res.update(check_if_scrap_warehouse_mandatory(res["bom_no"])) + + return res + +@frappe.whitelist() +def check_if_scrap_warehouse_mandatory(bom_no): + res = {"set_scrap_wh_mandatory": False } + if bom_no: + bom = frappe.get_doc("BOM", bom_no) + + if len(bom.scrap_items) > 0: + res["set_scrap_wh_mandatory"] = True + + return res + +@frappe.whitelist() +def set_production_order_ops(name): + po = frappe.get_doc('Production Order', name) + po.set_production_order_operations() + po.save() + +@frappe.whitelist() +def make_stock_entry(production_order_id, purpose, qty=None): + production_order = frappe.get_doc("Production Order", production_order_id) + if not frappe.db.get_value("Warehouse", production_order.wip_warehouse, "is_group") \ + and not production_order.skip_transfer: + wip_warehouse = production_order.wip_warehouse + else: + wip_warehouse = None + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.purpose = purpose + stock_entry.production_order = production_order_id + stock_entry.company = production_order.company + stock_entry.from_bom = 1 + stock_entry.bom_no = production_order.bom_no + 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)) + + if purpose=="Material Transfer for Manufacture": + stock_entry.to_warehouse = wip_warehouse + stock_entry.project = production_order.project + else: + stock_entry.from_warehouse = wip_warehouse + stock_entry.to_warehouse = production_order.fg_warehouse + additional_costs = get_additional_costs(production_order, fg_qty=stock_entry.fg_completed_qty) + stock_entry.project = production_order.project + stock_entry.set("additional_costs", additional_costs) + + stock_entry.get_items() + return stock_entry.as_dict() + +@frappe.whitelist() +def make_timesheet(production_order, company): + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = "" + timesheet.production_order = production_order + timesheet.company = company + return timesheet + +@frappe.whitelist() +def add_timesheet_detail(timesheet, args): + if isinstance(timesheet, unicode): + timesheet = frappe.get_doc('Timesheet', timesheet) + + if isinstance(args, unicode): + args = json.loads(args) + + timesheet.append('time_logs', args) + return timesheet + +@frappe.whitelist() +def get_default_warehouse(): + wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", + "default_wip_warehouse") + fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", + "default_fg_warehouse") + return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse} + +@frappe.whitelist() +def make_new_timesheet(source_name, target_doc=None): + po = frappe.get_doc('Production Order', source_name) + ts = po.make_time_logs(open_new=True) + + if not ts or not ts.get('time_logs'): + frappe.throw(_("Already completed")) + + return ts + +@frappe.whitelist() +def stop_unstop(production_order, status): + """ Called from client side on Stop/Unstop event""" + + if not frappe.has_permission("Production Order", "write"): + frappe.throw(_("Not permitted"), frappe.PermissionError) + + pro_order = frappe.get_doc("Production Order", production_order) + pro_order.update_status(status) + pro_order.update_planned_qty() + frappe.msgprint(_("Production Order has been {0}").format(status)) + pro_order.notify_update() + + return pro_order.status + +@frappe.whitelist() +def query_sales_order(production_item): + out = frappe.db.sql_list(""" + select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item + where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1 + union + select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item + where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1 + """, (production_item, production_item)) + + return out diff --git a/erpnext/modules.txt b/erpnext/modules.txt index d469145304d..fce0dcddb9a 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -11,7 +11,6 @@ Support Utilities Shopping Cart Assets -Hub Node Portal Maintenance Education @@ -21,4 +20,4 @@ Restaurant Agriculture ERPNext Integrations Non Profit -Hotels \ No newline at end of file +Hotels diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3b3c25b69a8..6b4d1f7e235 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -563,3 +563,4 @@ erpnext.patches.v10_0.set_discount_amount erpnext.patches.v10_0.recalculate_gross_margin_for_project erpnext.patches.v11_0.make_job_card erpnext.patches.v11_0.redesign_healthcare_billing_work_flow +erpnext.patches.v10_0.delete_hub_documents diff --git a/erpnext/patches/v10_0/delete_hub_documents.py b/erpnext/patches/v10_0/delete_hub_documents.py new file mode 100644 index 00000000000..0d81bd52632 --- /dev/null +++ b/erpnext/patches/v10_0/delete_hub_documents.py @@ -0,0 +1,17 @@ + +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + for dt, dn in (("Page", "Hub"), ("DocType", "Hub Settings"), ("DocType", "Hub Category")): + frappe.delete_doc(dt, dn, ignore_missing=True) + + if frappe.db.exists("DocType", "Data Migration Plan"): + data_migration_plans = frappe.get_all("Data Migration Plan", filters={"module": 'Hub Node'}) + for plan in data_migration_plans: + plan_doc = frappe.get_doc("Data Migration Plan", plan.name) + for m in plan_doc.get("mappings"): + frappe.delete_doc("Data Migration Mapping", m.mapping, force=True) + frappe.delete_doc("Data Migration Plan", plan.name) + + frappe.delete_doc("Module Def", "Hub Node", ignore_missing=True) diff --git a/erpnext/patches/v10_0/setup_vat_for_uae_and_saudi_arabia.py b/erpnext/patches/v10_0/setup_vat_for_uae_and_saudi_arabia.py index 587fee1f9a8..a8d90499d88 100644 --- a/erpnext/patches/v10_0/setup_vat_for_uae_and_saudi_arabia.py +++ b/erpnext/patches/v10_0/setup_vat_for_uae_and_saudi_arabia.py @@ -7,7 +7,6 @@ from erpnext.setup.doctype.company.company import install_country_fixtures def execute(): frappe.reload_doc("accounts", "doctype", "account") - frappe.reload_doc("hub_node", "doctype", "hub_category") frappe.reload_doc("accounts", "doctype", "payment_schedule") for d in frappe.get_all('Company', filters={'country': ('in', ['Saudi Arabia', 'United Arab Emirates'])}): diff --git a/erpnext/patches/v8_1/setup_gst_india.py b/erpnext/patches/v8_1/setup_gst_india.py index a9133ae9a66..5370fa2aa54 100644 --- a/erpnext/patches/v8_1/setup_gst_india.py +++ b/erpnext/patches/v8_1/setup_gst_india.py @@ -4,7 +4,6 @@ from frappe.email import sendmail_to_system_managers def execute(): frappe.reload_doc('stock', 'doctype', 'item') frappe.reload_doc("stock", "doctype", "customs_tariff_number") - frappe.reload_doc("hub_node", "doctype", "hub_category") frappe.reload_doc("accounts", "doctype", "payment_terms_template") frappe.reload_doc("accounts", "doctype", "payment_schedule") diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 2851537ed99..5d03686172e 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -3951,7 +3951,7 @@ "issingle": 0, "istable": 0, "max_attachments": 1, - "modified": "2018-08-30 05:28:12.312880", + "modified": "2018-09-06 14:45:48.715529", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b7dbda2647d..1a94c04ad2f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -149,6 +149,16 @@ class StockEntry(StockController): and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1) amount = amount[0][0] if amount else 0 + additional_costs = frappe.db.sql(""" select ifnull(sum(sed.amount), 0) + from + `tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed + where + se.docstatus = 1 and se.project = %s and sed.parent = se.name + and se.purpose = 'Manufacture'""", self.project, as_list=1) + + additional_cost_amt = additional_costs[0][0] if additional_costs else 0 + + amount += additional_cost_amt frappe.db.set_value('Project', self.project, 'total_consumed_material_cost', amount) def validate_item(self): diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 58255d4fada..d29ea9c7301 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -110,7 +110,7 @@ class StockReconciliation(StockController): self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed"))) - if row.qty and not row.valuation_rate: + if row.qty and row.valuation_rate in ["", None]: row.valuation_rate = get_stock_balance(row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True)[1] if not row.valuation_rate: diff --git a/erpnext/templates/print_formats/includes/item_table_qty.html b/erpnext/templates/print_formats/includes/item_table_qty.html index 0c800698854..239859eea19 100644 --- a/erpnext/templates/print_formats/includes/item_table_qty.html +++ b/erpnext/templates/print_formats/includes/item_table_qty.html @@ -1,4 +1,6 @@ -{% if (doc.stock_uom and not doc.is_print_hide("stock_uom")) or (doc.uom and not doc.is_print_hide("uom")) -%} -{{ _(doc.uom or doc.stock_uom) }} +{% if (doc.uom and not doc.is_print_hide("uom")) %} + {{ _(doc.uom) }} +{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} + {{ _(doc.stock_uom) }} {%- endif %} {{ doc.get_formatted("qty", doc) }}