[resolved] merge-conflicts
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 45b25f6..534489c 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@
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 cbbc9e9..0d22c44 100644
--- a/erpnext/hr/doctype/salary_slip/salary_slip.js
+++ b/erpnext/hr/doctype/salary_slip/salary_slip.js
@@ -122,14 +122,20 @@
// 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 e69de29..0000000
--- a/erpnext/hub_node/data_migration_mapping/__init__.py
+++ /dev/null
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 e69de29..0000000
--- a/erpnext/hub_node/data_migration_mapping/company_to_hub_company/__init__.py
+++ /dev/null
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 0ea1bc4..0000000
--- 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 e69de29..0000000
--- a/erpnext/hub_node/data_migration_mapping/item_to_hub_item/__init__.py
+++ /dev/null
diff --git a/erpnext/hub_node/doctype/__init__.py b/erpnext/hub_node/doctype/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/hub_node/doctype/__init__.py
+++ /dev/null
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 04f9717..c91bb8f 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 @@
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 0000000..2f2c40e
--- /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 d469145..fce0dcd 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -11,7 +11,6 @@
Utilities
Shopping Cart
Assets
-Hub Node
Portal
Maintenance
Education
@@ -21,4 +20,4 @@
Agriculture
ERPNext Integrations
Non Profit
-Hotels
\ No newline at end of file
+Hotels
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 3b3c25b..6b4d1f7 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -563,3 +563,4 @@
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 0000000..0d81bd5
--- /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 587fee1..a8d9049 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 @@
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 a9133ae..5370fa2 100644
--- a/erpnext/patches/v8_1/setup_gst_india.py
+++ b/erpnext/patches/v8_1/setup_gst_india.py
@@ -4,7 +4,6 @@
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 2851537..5d03686 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 b7dbda2..1a94c04 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -149,6 +149,16 @@
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 58255d4..d29ea9c 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -110,7 +110,7 @@
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 0c80069..239859e 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")) -%}
-<small class="pull-left">{{ _(doc.uom or doc.stock_uom) }}</small>
+{% if (doc.uom and not doc.is_print_hide("uom")) %}
+ <small class="pull-left">{{ _(doc.uom) }}</small>
+{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %}
+ <small class="pull-left">{{ _(doc.stock_uom) }}</small>
{%- endif %}
{{ doc.get_formatted("qty", doc) }}