feat(shipment): Shipment Doctype with Integrations
diff --git a/erpnext/erpnext_integrations/doctype/letmeship/__init__.py b/erpnext/erpnext_integrations/doctype/letmeship/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/letmeship/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js
new file mode 100644
index 0000000..1e5e372
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('LetMeShip', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json
new file mode 100644
index 0000000..4a9a70f
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "creation": "2020-07-23 10:55:19.669830",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "enabled",
+  "api_id",
+  "api_password"
+ ],
+ "fields": [
+  {
+   "default": "0",
+   "fieldname": "enabled",
+   "fieldtype": "Check",
+   "label": "Enabled"
+  },
+  {
+   "fieldname": "api_id",
+   "fieldtype": "Data",
+   "label": "API ID",
+   "read_only_depends_on": "eval:doc.enabled == 0"
+  },
+  {
+   "fieldname": "api_password",
+   "fieldtype": "Data",
+   "label": "API Password",
+   "read_only_depends_on": "eval:doc.enabled == 0"
+  }
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-08-05 16:33:44.548230",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "LetMeShip",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py
new file mode 100644
index 0000000..3ad06db
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py
@@ -0,0 +1,396 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import requests
+import frappe
+import json
+import re
+from frappe import _
+from frappe.model.document import Document
+from erpnext.erpnext_integrations.utils import get_tracking_url
+
+LETMESHIP_PROVIDER = 'LetMeShip'
+
+class LetMeShip(Document):
+	pass
+
+def get_letmeship_available_services(delivery_to_type, pickup_address,
+	delivery_address, shipment_parcel, description_of_content, pickup_date,
+	value_of_goods, pickup_contact=None, delivery_contact=None):
+	# Retrieve rates at LetMeShip from specification stated.
+	enabled = frappe.db.get_single_value('LetMeShip','enabled')
+	api_id = frappe.db.get_single_value('LetMeShip','api_id')
+	api_password = frappe.db.get_single_value('LetMeShip','api_password')
+	if not enabled or not api_id or not api_password:
+		return []
+
+	set_letmeship_specific_fields(pickup_contact, delivery_contact)
+
+	# LetMeShip have limit of 30 characters for Company field
+	if len(pickup_address.address_title) > 30:
+		pickup_address.address_title = pickup_address.address_title[:30]
+	if len(delivery_address.address_title) > 30:
+		delivery_address.address_title = delivery_address.address_title[:30]
+	parcel_list = get_parcel_list(json.loads(shipment_parcel), description_of_content)
+
+	url = 'https://api.letmeship.com/v1/available'
+	headers = {
+		'Content-Type': 'application/json',
+		'Accept': 'application/json',
+		'Access-Control-Allow-Origin': 'string'
+	}
+	payload = {'pickupInfo': {
+		'address': {
+			'countryCode': pickup_address.country_code,
+			'zip': pickup_address.pincode,
+			'city': pickup_address.city,
+			'street': pickup_address.address_line1,
+			'addressInfo1': pickup_address.address_line2,
+			'houseNo': '',
+		},
+		'company': pickup_address.address_title,
+		'person': {
+			'title': pickup_contact.title,
+			'firstname': pickup_contact.first_name,
+			'lastname': pickup_contact.last_name
+		},
+		'phone': {
+			'phoneNumber': pickup_contact.phone,
+			'phoneNumberPrefix': pickup_contact.phone_prefix
+		},
+		'email': pickup_contact.email,
+	}, 'deliveryInfo': {
+		'address': {
+			'countryCode': delivery_address.country_code,
+			'zip': delivery_address.pincode,
+			'city': delivery_address.city,
+			'street': delivery_address.address_line1,
+			'addressInfo1': delivery_address.address_line2,
+			'houseNo': '',
+		},
+		'company': delivery_address.address_title,
+		'person': {
+			'title': delivery_contact.title,
+			'firstname': delivery_contact.first_name,
+			'lastname': delivery_contact.last_name
+		},
+		'phone': {
+			'phoneNumber': delivery_contact.phone,
+			'phoneNumberPrefix': delivery_contact.phone_prefix
+		},
+		'email': delivery_contact.email,
+	}, 'shipmentDetails': {
+		'contentDescription': description_of_content,
+		'shipmentType': 'PARCEL',
+		'shipmentSettings': {
+			'saturdayDelivery': False,
+			'ddp': False,
+			'insurance': False,
+			'pickupOrder': False,
+			'pickupTailLift': False,
+			'deliveryTailLift': False,
+			'holidayDelivery': False,
+		},
+		'goodsValue': value_of_goods,
+		'parcelList': parcel_list,
+		'pickupInterval': {'date': pickup_date},
+	}}
+	try:
+		available_services = []
+		response_data = requests.post(
+			url=url,
+			auth=(api_id, api_password), 
+			headers=headers,
+			data=json.dumps(payload)
+		)
+		response_data = json.loads(response_data.text)
+		if 'serviceList' in response_data:
+			for response in response_data['serviceList']:
+				available_service = frappe._dict()
+				basic_info = response['baseServiceDetails']
+				price_info = basic_info['priceInfo']
+				available_service.service_provider = LETMESHIP_PROVIDER
+				available_service.id = basic_info['id']
+				available_service.carrier = basic_info['carrier']
+				available_service.carrier_name = basic_info['name']
+				available_service.service_name = ''
+				available_service.is_preferred = 0
+				available_service.real_weight = price_info['realWeight']
+				available_service.total_price = price_info['netPrice']
+				available_service.price_info = price_info
+				available_services.append(available_service)
+			return available_services
+		else:
+			frappe.throw(
+				_('Error occurred while fetching LetMeShip prices: {0}')
+					.format(response_data['message'])
+			)
+	except Exception as exc:
+		frappe.msgprint(
+			_('Error occurred while fetching LetMeShip Prices: {0}')
+				.format(str(exc)),
+			indicator='orange',
+			alert=True
+		)
+	return []
+
+
+def create_letmeship_shipment(pickup_address, delivery_address, shipment_parcel, description_of_content,
+	pickup_date, value_of_goods, service_info, shipment_notific_email, tracking_notific_email,
+	pickup_contact=None, delivery_contact=None):
+	# Create a transaction at LetMeShip
+	# LetMeShip have limit of 30 characters for Company field
+	enabled = frappe.db.get_single_value('LetMeShip','enabled')
+	api_id = frappe.db.get_single_value('LetMeShip','api_id')
+	api_password = frappe.db.get_single_value('LetMeShip','api_password')
+	if not enabled or not api_id or not api_password:
+		return []
+
+	set_letmeship_specific_fields(pickup_contact, delivery_contact)
+
+	if len(pickup_address.address_title) > 30:
+		pickup_address.address_title = pickup_address.address_title[:30]
+	if len(delivery_address.address_title) > 30:
+		delivery_address.address_title = delivery_address.address_title[:30]
+
+	parcel_list = get_parcel_list(json.loads(shipment_parcel), description_of_content)
+	url = 'https://api.letmeship.com/v1/shipments'
+	headers = {
+		'Content-Type': 'application/json',
+		'Accept': 'application/json',
+		'Access-Control-Allow-Origin': 'string'
+	}
+	payload = {
+		'pickupInfo': {
+			'address': {
+				'countryCode': pickup_address.country_code,
+				'zip': pickup_address.pincode,
+				'city': pickup_address.city,
+				'street': pickup_address.address_line1,
+				'addressInfo1': pickup_address.address_line2,
+				'houseNo': '',
+			},
+			'company': pickup_address.address_title,
+			'person': {
+				'title': pickup_contact.title,
+				'firstname': pickup_contact.first_name,
+				'lastname': pickup_contact.last_name
+			},
+			'phone': {
+				'phoneNumber': pickup_contact.phone,
+				'phoneNumberPrefix': pickup_contact.phone_prefix
+			},
+			'email': pickup_contact.email,
+		},
+		'deliveryInfo': {
+			'address': {
+				'countryCode': delivery_address.country_code,
+				'zip': delivery_address.pincode,
+				'city': delivery_address.city,
+				'street': delivery_address.address_line1,
+				'addressInfo1': delivery_address.address_line2,
+				'houseNo': '',
+			},
+			'company': delivery_address.address_title,
+			'person': {
+				'title': delivery_contact.title,
+				'firstname': delivery_contact.first_name,
+				'lastname': delivery_contact.last_name
+			},
+			'phone': {
+				'phoneNumber': delivery_contact.phone,
+				'phoneNumberPrefix': delivery_contact.phone_prefix
+			},
+			'email': delivery_contact.email,
+		},
+		'service': {
+			'baseServiceDetails': {
+				'id': service_info['id'],
+				'name': service_info['service_name'],
+				'carrier': service_info['carrier'],
+				'priceInfo': service_info['price_info'],
+			},
+			'supportedExWorkType': [],
+			'messages': [''],
+			'description': '',
+			'serviceInfo': '',
+		},
+		'shipmentDetails': {
+			'contentDescription': description_of_content,
+			'shipmentType': 'PARCEL',
+			'shipmentSettings': {
+				'saturdayDelivery': False,
+				'ddp': False,
+				'insurance': False,
+				'pickupOrder': False,
+				'pickupTailLift': False,
+				'deliveryTailLift': False,
+				'holidayDelivery': False,
+			},
+			'goodsValue': value_of_goods,
+			'parcelList': parcel_list,
+			'pickupInterval': {
+				'date': pickup_date
+			},
+			'contentDescription': description_of_content,
+		},
+		'shipmentNotification': {
+			'trackingNotification': {
+				'deliveryNotification': True,
+				'problemNotification': True,
+				'emails': [tracking_notific_email],
+				'notificationText': '',
+			}, 
+			'recipientNotification': {
+				'notificationText': '',
+				'emails': [ shipment_notific_email ]
+			}
+		},
+		'labelEmail': True,
+	}
+	try:
+		response_data = requests.post(
+			url=url,
+			auth=(api_id, api_password),
+			headers=headers,
+			data=json.dumps(payload)
+		)
+		response_data = json.loads(response_data.text)
+		if 'shipmentId' in response_data:
+			shipment_amount = response_data['service']['priceInfo']['totalPrice']
+			awb_number = ''
+			url = 'https://api.letmeship.com/v1/shipments/{id}'.format(id=response_data['shipmentId'])
+			tracking_response = requests.get(url, auth=(api_id, api_password),headers=headers)
+			tracking_response_data = json.loads(tracking_response.text)
+			if 'trackingData' in tracking_response_data:
+				for parcel in tracking_response_data['trackingData']['parcelList']:
+					if 'awbNumber' in parcel:
+						awb_number = parcel['awbNumber']
+			return {
+				'service_provider': LETMESHIP_PROVIDER,
+				'shipment_id': response_data['shipmentId'],
+				'carrier': service_info['carrier'],
+				'carrier_service': service_info['service_name'],
+				'shipment_amount': shipment_amount,
+				'awb_number': awb_number,
+			}
+		elif 'message' in response_data:
+			frappe.throw(
+				_('Error occurred while creating Shipment: {0}')
+					.format(response_data['message'])
+			)
+	except Exception as exc:
+		frappe.msgprint(
+			_('Error occurred while creating Shipment: {0}')
+				.format(str(exc)),
+			indicator='orange',
+			alert=True
+		)
+
+
+def get_letmeship_label(shipment_id):
+	# Retrieve shipment label from LetMeShip
+	api_id = frappe.db.get_single_value('LetMeShip','api_id')
+	api_password = frappe.db.get_single_value('LetMeShip','api_password')
+	headers = {
+		'Content-Type': 'application/json',
+		'Accept': 'application/json',
+		'Access-Control-Allow-Origin': 'string'
+	}
+	url = 'https://api.letmeship.com/v1/shipments/{id}/documents?types=LABEL'\
+		.format(id=shipment_id)
+	shipment_label_response = requests.get(
+		url,
+		auth=(api_id,api_password),
+		headers=headers
+	)
+	shipment_label_response_data = json.loads(shipment_label_response.text)
+	if 'documents' in shipment_label_response_data:
+		for label in shipment_label_response_data['documents']:
+			if 'data' in label:
+				return json.dumps(label['data'])
+	else:
+		frappe.throw(
+			_('Error occurred while printing Shipment: {0}')
+				.format(shipment_label_response_data['message'])
+		)
+
+
+def get_letmeship_tracking_data(shipment_id):
+	# return letmeship tracking data
+	api_id = frappe.db.get_single_value('LetMeShip','api_id')
+	api_password = frappe.db.get_single_value('LetMeShip','api_password')
+	headers = {
+		'Content-Type': 'application/json',
+		'Accept': 'application/json',
+		'Access-Control-Allow-Origin': 'string'
+	}
+	try:
+		url = 'https://api.letmeship.com/v1/tracking?shipmentid={id}'.format(id=shipment_id)
+		tracking_data_response = requests.get(
+			url,
+			auth=(api_id, api_password),
+			headers=headers
+		)
+		tracking_data = json.loads(tracking_data_response.text)
+		if 'awbNumber' in tracking_data:
+			tracking_status = 'In Progress'
+			if tracking_data['lmsTrackingStatus'].startswith('DELIVERED'):
+				tracking_status = 'Delivered'
+			if tracking_data['lmsTrackingStatus'] == 'RETURNED':
+				tracking_status = 'Returned'
+			if tracking_data['lmsTrackingStatus'] == 'LOST':
+				tracking_status = 'Lost'
+			tracking_url = get_tracking_url(
+				carrier=tracking_data['carrier'],
+				tracking_number=tracking_data['awbNumber']
+			)
+			return {
+				'awb_number': tracking_data['awbNumber'],
+				'tracking_status': tracking_status,
+				'tracking_status_info': tracking_data['lmsTrackingStatus'],
+				'tracking_url': tracking_url,
+			}
+		elif 'message' in tracking_data:
+			frappe.throw(
+				_('Error occurred while updating Shipment: {0}')
+					.format(tracking_data['message'])
+			)
+	except Exception as exc:
+		frappe.msgprint(
+			_('Error occurred while updating Shipment: {0}')
+				.format(str(exc)),
+			indicator='orange',
+			alert=True
+		)
+
+
+def get_parcel_list(shipment_parcel, description_of_content):
+	parcel_list = []
+	for parcel in shipment_parcel:
+		formatted_parcel = {}
+		formatted_parcel['height'] = parcel.get('height')
+		formatted_parcel['width'] = parcel.get('width')
+		formatted_parcel['length'] = parcel.get('length')
+		formatted_parcel['weight'] = parcel.get('weight')
+		formatted_parcel['quantity'] = parcel.get('count')
+		formatted_parcel['contentDescription'] = description_of_content
+		parcel_list.append(formatted_parcel)
+	return parcel_list
+
+def set_letmeship_specific_fields(pickup_contact, delivery_contact):
+	pickup_contact.phone_prefix = pickup_contact.phone[:3]
+	pickup_contact.phone = re.sub('[^A-Za-z0-9]+', '', pickup_contact.phone[3:])
+
+	pickup_contact.title = 'MS'
+	if pickup_contact.gender == 'Male':
+		pickup_contact.title = 'MR'
+
+	delivery_contact.phone_prefix = delivery_contact.phone[:3]
+	delivery_contact.phone = re.sub('[^A-Za-z0-9]+', '', delivery_contact.phone[3:])
+
+	delivery_contact.title = 'MS'
+	if delivery_contact.gender == 'Male':
+		delivery_contact.title = 'MR'
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py b/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py
new file mode 100644
index 0000000..3439e4f
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestLetMeShip(unittest.TestCase):
+	pass
diff --git a/erpnext/erpnext_integrations/doctype/packlink/__init__.py b/erpnext/erpnext_integrations/doctype/packlink/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/packlink/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.js b/erpnext/erpnext_integrations/doctype/packlink/packlink.js
new file mode 100644
index 0000000..da86458
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/packlink/packlink.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Packlink', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.json b/erpnext/erpnext_integrations/doctype/packlink/packlink.json
new file mode 100644
index 0000000..a56595e
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/packlink/packlink.json
@@ -0,0 +1,48 @@
+{
+ "actions": [],
+ "creation": "2020-07-22 10:45:17.672439",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "enabled",
+  "api_key"
+ ],
+ "fields": [
+  {
+   "default": "0",
+   "fieldname": "enabled",
+   "fieldtype": "Check",
+   "label": "Enabled"
+  },
+  {
+   "fieldname": "api_key",
+   "fieldtype": "Data",
+   "label": "API Key",
+   "read_only_depends_on": "eval:doc.enabled == 0"
+  }
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-08-05 16:33:59.720980",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "Packlink",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.py b/erpnext/erpnext_integrations/doctype/packlink/packlink.py
new file mode 100644
index 0000000..7fdb053
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/packlink/packlink.py
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import json
+import frappe
+import requests
+from frappe import _
+from frappe.model.document import Document
+from erpnext.erpnext_integrations.utils import get_tracking_url
+
+PACKLINK_PROVIDER = 'Packlink'
+
+class Packlink(Document):
+	pass
+
+def get_packlink_available_services(pickup_address, delivery_address, shipment_parcel,pickup_date):
+	# Retrieve rates at PackLink from specification stated.
+	from_zip = pickup_address.pincode
+	from_country_code = pickup_address.country_code
+	to_zip = delivery_address.pincode
+	to_country_code = delivery_address.country_code
+	shipment_parcel_params = ''
+	parcel_list = packlink_get_parcel_list(json.loads(shipment_parcel))
+	for (index, parcel) in enumerate(parcel_list):
+		shipment_parcel_params += 'packages[{index}][height]={height}&packages[{index}][length]={length}&packages[{index}][weight]={weight}&packages[{index}][width]={width}&'.format(
+			index=index,
+			height=parcel['height'],
+			length=parcel['length'],
+			weight=parcel['weight'],
+			width=parcel['width']
+		)
+	url = 'https://api.packlink.com/v1/services?from[country]={}&from[zip]={}&to[country]={}&to[zip]={}&{}sortBy=totalPrice&source=PRO'.format(
+		from_country_code,
+		from_zip,
+		to_country_code,
+		to_zip,
+		shipment_parcel_params
+	)
+	api_key = frappe.db.get_single_value('Packlink', 'api_key')
+	enabled = frappe.db.get_single_value('Packlink', 'enabled')
+	if not api_key or not enabled:
+		return []
+	try:
+		responses = requests.get(url, headers={'Authorization': api_key})
+		responses_dict = json.loads(responses.text)
+		# If an error occured on the api. Show the error message
+		if 'messages' in responses_dict:
+			frappe.msgprint(
+				_('Packlink: {0}'
+					.format(str(responses_dict['messages'][0]['message']))
+				),
+				indicator='orange',
+				alert=True
+			)
+		available_services = []
+		for response in responses_dict:
+			if parse_pickup_date(pickup_date) \
+					in response['available_dates'].keys():
+				available_service = frappe._dict()
+				available_service.service_provider = PACKLINK_PROVIDER
+				available_service.carrier = response['carrier_name']
+				available_service.carrier_name = response['name']
+				available_service.service_name = ''
+				available_service.is_preferred = 0
+				available_service.total_price = response['price']['base_price']
+				available_service.actual_price = response['price']['total_price']
+				available_service.service_id = response['id']
+				available_service.available_dates = response['available_dates']
+				available_services.append(available_service)
+
+		return available_services
+	except Exception as exc:
+		frappe.msgprint(
+			_('Error occurred on Packlink: {0}')
+				.format(str(exc)), indicator='orange',
+			alert=True
+		)
+	return []
+
+
+def create_packlink_shipment(pickup_address, delivery_address, shipment_parcel,
+	description_of_content, pickup_date, value_of_goods, pickup_contact,
+	delivery_contact, service_info):
+	# Create a transaction at PackLink
+	enabled = frappe.db.get_single_value('Packlink', 'enabled')
+	if not enabled:
+		frappe.throw(_('Packlink integration is not enabled'))
+	api_key = frappe.db.get_single_value('Packlink', 'api_key')
+	from_country_code = pickup_address.country_code
+	to_country_code = delivery_address.country_code
+	data = {
+		'additional_data': {
+			'postal_zone_id_from': '',
+			'postal_zone_name_from': pickup_address.country,
+			'postal_zone_id_to': '',
+			'postal_zone_name_to': delivery_address.country,
+		},
+		'collection_date': parse_pickup_date(pickup_date),
+		'collection_time': '',
+		'content': description_of_content,
+		'contentvalue': value_of_goods,
+		'content_second_hand': False,
+		'from': {
+			'city': pickup_address.city,
+			'company': pickup_address.address_title,
+			'country': from_country_code,
+			'email': pickup_contact.email,
+			'name': pickup_contact.first_name,
+			'phone': pickup_contact.phone,
+			'state': pickup_address.country,
+			'street1': pickup_address.address_line1,
+			'street2': pickup_address.address_line2,
+			'surname': pickup_contact.last_name,
+			'zip_code': pickup_address.pincode,
+		},
+		'insurance': {'amount': 0, 'insurance_selected': False},
+		'price': {},
+		'packages': packlink_get_parcel_list(json.loads(shipment_parcel)),
+		'service_id': service_info['service_id'],
+		'to': {
+			'city': delivery_address.city,
+			'company': delivery_address.address_title,
+			'country': to_country_code,
+			'email': delivery_contact.email,
+			'name': delivery_contact.first_name,
+			'phone': delivery_contact.phone,
+			'state': delivery_address.country,
+			'street1': delivery_address.address_line1,
+			'street2': delivery_address.address_line2,
+			'surname': delivery_contact.last_name,
+			'zip_code': delivery_address.pincode,
+		},
+	}
+
+	url = 'https://api.packlink.com/v1/shipments'
+	headers = {
+		'Authorization': api_key,
+		'Content-Type': 'application/json'
+	}
+	try:
+		response_data = requests.post(url, json=data, headers=headers)
+		response_data = json.loads(response_data.text)
+		if 'reference' in response_data:
+			return {
+				'service_provider': PACKLINK_PROVIDER,
+				'shipment_id': response_data['reference'],
+				'carrier': service_info['carrier'],
+				'carrier_service': service_info['service_name'],
+				'shipment_amount': service_info['actual_price'],
+				'awb_number': '',
+			}
+	except Exception as exc:
+		frappe.msgprint(
+			_('Error occurred while creating Shipment: {0}')
+				.format(str(exc)),
+			indicator='orange',
+			alert=True
+		)
+
+
+def get_packlink_label(shipment_id):
+	# Retrieve shipment label from PackLink
+	enabled = frappe.db.get_single_value('Packlink', 'enabled')
+	if not enabled:
+		frappe.throw(_('Packlink integration is not enabled'))
+	api_key = frappe.db.get_single_value('Packlink', 'api_key')
+	headers = {
+		'Authorization': api_key,
+		'Content-Type': 'application/json'
+	}
+	shipment_label_response = requests.get(
+		'https://api.packlink.com/v1/shipments/{id}/labels'.format(id=shipment_id),
+		headers=headers
+	)
+	shipment_label = json.loads(shipment_label_response.text)
+	if shipment_label:
+		return shipment_label
+	else:
+		frappe.msgprint(_('Shipment ID not found'))
+
+
+def get_packlink_tracking_data(shipment_id):
+	# Get Packlink Tracking Info
+	enabled = frappe.db.get_single_value('Packlink', 'enabled')
+	if not enabled:
+		frappe.throw(_('Packlink integration is not enabled'))
+	api_key = frappe.db.get_single_value('Packlink', 'api_key')
+	headers = {
+		'Authorization': api_key,
+		'Content-Type': 'application/json'
+	}
+	try:
+		url = 'https://api.packlink.com/v1/shipments/{id}'.format(id=shipment_id)
+		tracking_data_response = requests.get(url, headers=headers)
+		tracking_data = json.loads(tracking_data_response.text)
+		if 'trackings' in tracking_data:
+			tracking_status = 'In Progress'
+			if tracking_data['state'] == 'DELIVERED':
+				tracking_status = 'Delivered'
+			if tracking_data['state'] == 'RETURNED':
+				tracking_status = 'Returned'
+			if tracking_data['state'] == 'LOST':
+				tracking_status = 'Lost'
+			awb_number = None if not tracking_data['trackings'] else tracking_data['trackings'][0]
+			tracking_url = get_tracking_url(
+				carrier=tracking_data['carrier'],
+				tracking_number=awb_number
+			)
+			return {
+				'awb_number': awb_number,
+				'tracking_status': tracking_status,
+				'tracking_status_info': tracking_data['state'],
+				'tracking_url': tracking_url
+			}
+	except Exception as exc:
+		frappe.msgprint(_('Error occurred while updating Shipment: {0}').format(
+			str(exc)), indicator='orange', alert=True)
+	return []
+
+
+def packlink_get_parcel_list(shipment_parcel):
+	parcel_list = []
+	for parcel in shipment_parcel:
+		for count in range(parcel.get('count')):
+			formatted_parcel = {}
+			formatted_parcel['height'] = parcel.get('height')
+			formatted_parcel['width'] = parcel.get('width')
+			formatted_parcel['length'] = parcel.get('length')
+			formatted_parcel['weight'] = parcel.get('weight')
+			parcel_list.append(formatted_parcel)
+	return parcel_list
+
+
+def parse_pickup_date(pickup_date):
+	return pickup_date.replace('-', '/')
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py b/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py
new file mode 100644
index 0000000..106ae51
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestPacklink(unittest.TestCase):
+	pass
diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/__init__.py b/erpnext/erpnext_integrations/doctype/sendcloud/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/sendcloud/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js
new file mode 100644
index 0000000..3b85236
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('SendCloud', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json
new file mode 100644
index 0000000..dab54cb
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json
@@ -0,0 +1,56 @@
+{
+ "actions": [],
+ "creation": "2020-08-18 09:48:50.836233",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "enabled",
+  "api_key",
+  "api_secret"
+ ],
+ "fields": [
+  {
+   "default": "0",
+   "fieldname": "enabled",
+   "fieldtype": "Check",
+   "label": "Enabled"
+  },
+  {
+   "fieldname": "api_key",
+   "fieldtype": "Data",
+   "label": "API Key",
+   "read_only_depends_on": "eval:doc.enabled == 0"
+  },
+  {
+   "fieldname": "api_secret",
+   "fieldtype": "Data",
+   "label": "API Secret",
+   "read_only_depends_on": "eval:doc.enabled == 0"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-08-18 09:48:50.836233",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "SendCloud",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py
new file mode 100644
index 0000000..85c9438
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py
@@ -0,0 +1,171 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import requests
+import frappe
+import json
+from frappe import _
+from frappe.model.document import Document
+
+SENDCLOUD_PROVIDER = 'SendCloud'
+
+class SendCloud(Document):
+	pass
+
+def get_sendcloud_available_services(delivery_address, shipment_parcel):
+	# Retrieve rates at SendCloud from specification stated.
+	enabled = frappe.db.get_single_value('SendCloud', 'enabled')
+	api_key = frappe.db.get_single_value('SendCloud', 'api_key')
+	api_secret = frappe.db.get_single_value('SendCloud', 'api_secret')
+	if not enabled or not api_key or not api_secret:
+		return []
+
+	try:
+		url = 'https://panel.sendcloud.sc/api/v2/shipping_methods'
+		responses = requests.get(url, auth=(api_key, api_secret))
+		responses_dict = json.loads(responses.text)
+
+		available_services = []
+		for service in responses_dict['shipping_methods']:
+			for country in service['countries']:
+				if country['iso_2'] == delivery_address.country_code:
+					available_service = frappe._dict()
+					available_service.service_provider = 'SendCloud'
+					available_service.carrier = service['carrier']
+					available_service.service_name = service['name']
+					available_service.total_price = total_parcel_price(country['price'], json.loads(shipment_parcel))
+					available_service.service_id = service['id']
+					available_services.append(available_service)
+		return available_services
+	except Exception as exc:
+		frappe.msgprint(_('Error occurred on SendCloud: {0}').format(
+			str(exc)), indicator='orange', alert=True)
+
+def create_sendcloud_shipment(
+	shipment,
+	delivery_address,
+	delivery_contact,
+	service_info,
+	shipment_parcel,
+	description_of_content,
+	value_of_goods
+):
+	# Create a transaction at SendCloud
+	enabled = frappe.db.get_single_value('SendCloud', 'enabled')
+	api_key = frappe.db.get_single_value('SendCloud', 'api_key')
+	api_secret = frappe.db.get_single_value('SendCloud', 'api_secret')
+	if not enabled or not api_key or not api_secret:
+		return []
+
+	parcels = []
+	for i, parcel in enumerate(json.loads(shipment_parcel), start=1):
+		parcel_data = {
+			'name': "{} {}".format(delivery_contact.first_name, delivery_contact.last_name),
+			'company_name': delivery_address.address_title,
+			'address': delivery_address.address_line1,
+			'address_2': delivery_address.address_line2 or '',
+			'city': delivery_address.city,
+			'postal_code': delivery_address.pincode,
+			'telephone': delivery_contact.phone,
+			'request_label': True,
+			'email': delivery_contact.email,
+			'data': [],
+			'country': delivery_address.country_code,
+			'shipment': {
+				'id': service_info['service_id']
+			},
+			'order_number': "{}-{}".format(shipment, i),
+			'external_reference': "{}-{}".format(shipment, i),
+			'weight': parcel.get('weight'),
+			'parcel_items': get_parcel_items(parcel, description_of_content, value_of_goods)
+		}
+		parcels.append(parcel_data)
+	data = {
+		'parcels': parcels
+	}
+	try:
+		url = 'https://panel.sendcloud.sc/api/v2/parcels?errors=verbose'
+		response_data = requests.post(url, json=data, auth=(api_key, api_secret))
+		response_data = json.loads(response_data.text)
+		if 'failed_parcels' in response_data:
+			frappe.msgprint(_('Error occurred while creating Shipment: {0}'
+									).format(response_data['failed_parcels'][0]['errors']), indicator='orange',
+								alert=True)
+		else:
+			shipment_id = ', '.join([str(x['id']) for x in response_data['parcels']])
+			awb_number = ', '.join([str(x['tracking_number']) for x in response_data['parcels']])
+			return {
+				'service_provider': 'SendCloud',
+				'shipment_id': shipment_id,
+				'carrier': service_info['carrier'],
+				'carrier_service': service_info['service_name'],
+				'shipment_amount': service_info['total_price'],
+				'awb_number': awb_number
+			}
+	except Exception as exc:
+		frappe.msgprint(_('Error occurred while creating Shipment: {0}').format(
+			str(exc)), indicator='orange', alert=True)
+
+def get_sendcloud_label(shipment_id):
+	# Retrieve shipment label from SendCloud
+	api_key = frappe.db.get_single_value('SendCloud', 'api_key')
+	api_secret = frappe.db.get_single_value('SendCloud', 'api_secret')
+	shipment_id_list = shipment_id.split(', ')
+	label_urls = []
+	for ship_id in shipment_id_list:
+		shipment_label_response = \
+			requests.get('https://panel.sendcloud.sc/api/v2/labels/{id}'.format(id=ship_id), auth=(api_key, api_secret))
+		shipment_label = json.loads(shipment_label_response.text)
+		label_urls.append(shipment_label['label']['label_printer'])
+	if len(label_urls):
+		return label_urls
+	else:
+		frappe.msgprint(_('Shipment ID not found'))
+
+def get_sendcloud_tracking_data(shipment_id):
+	# return SendCloud tracking data 
+	try:
+		api_key = frappe.db.get_single_value('SendCloud', 'api_key')
+		api_secret = frappe.db.get_single_value('SendCloud', 'api_secret')
+		shipment_id_list = shipment_id.split(', ')
+		tracking_url = ''
+		awb_number = []
+		tracking_status = []
+		tracking_status_info = []
+		for ship_id in shipment_id_list:
+			tracking_data_response = \
+				requests.get('https://panel.sendcloud.sc/api/v2/parcels/{id}'.format(id=ship_id), auth=(api_key, api_secret))
+			tracking_data = json.loads(tracking_data_response.text)
+			tracking_url_template = \
+				'<a href="{{ tracking_url }}" target="_blank"><b>{{ _("Click here to Track Shipment") }}</b></a><br>'
+			tracking_url += frappe.render_template(tracking_url_template, {'tracking_url': tracking_data['parcel']['tracking_url']})
+			awb_number.append(tracking_data['parcel']['tracking_number'])
+			tracking_status.append(tracking_data['parcel']['status']['message'])
+			tracking_status_info.append(tracking_data['parcel']['status']['message'])
+		return {
+			'awb_number': ', '.join(awb_number),
+			'tracking_status': ', '.join(tracking_status),
+			'tracking_status_info': ', '.join(tracking_status_info),
+			'tracking_url': tracking_url
+		}
+	except Exception as exc:
+		frappe.msgprint(_('Error occurred while updating Shipment: {0}').format(
+			str(exc)), indicator='orange', alert=True)
+
+def total_parcel_price(parcel_price, shipment_parcel):
+	count = 0
+	for parcel in shipment_parcel:
+		count += parcel.get('count')
+	return parcel_price * count
+
+def get_parcel_items(parcel, description_of_content, value_of_goods):
+	parcel_list = []
+	formatted_parcel = {}
+	formatted_parcel['description'] = description_of_content
+	formatted_parcel['quantity'] = parcel.get('count')
+	formatted_parcel['weight'] = parcel.get('weight')
+	formatted_parcel['value'] = value_of_goods
+	parcel_list.append(formatted_parcel)
+	return parcel_list
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py b/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py
new file mode 100644
index 0000000..5cbe80e
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestSendCloud(unittest.TestCase):
+	pass
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index e278fd7..e7ef4c8 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -60,4 +60,14 @@
 				"default_account": payment_gateway_account
 			}]
 		})
-		mode_of_payment.insert(ignore_permissions=True)
\ No newline at end of file
+		mode_of_payment.insert(ignore_permissions=True)
+
+def get_tracking_url(carrier, tracking_number):
+	# Return the formatted Tracking URL.
+	tracking_url = ''
+	url_reference = frappe.get_value('Parcel Service', carrier, 'url_reference')
+	if url_reference:
+		tracking_url = frappe.render_template(url_reference, {'tracking_number': tracking_number})
+		tracking_url_template =  '<a href="{{ tracking_url }}" target="_blank"><b>{{ _("Click here to Track Shipment") }}</a></b>'
+		tracking_url = frappe.render_template(tracking_url_template, {'tracking_url': tracking_url})
+	return tracking_url
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 251a26a..03921c5 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -156,6 +156,11 @@
 		}
 
 		if (!doc.is_return && doc.status!="Closed") {
+			if(doc.docstatus == 1) {
+				this.frm.add_custom_button(__('Shipment'), function() {
+					me.make_shipment() }, __('Create'));
+			}
+
 			if(flt(doc.per_installed, 2) < 100 && doc.docstatus==1)
 				this.frm.add_custom_button(__('Installation Note'), function() {
 					me.make_installation_note() }, __('Create'));
@@ -220,6 +225,13 @@
 		}
 	},
 
+	make_shipment: function() {
+		frappe.model.open_mapped_doc({
+			method: "erpnext.stock.doctype.delivery_note.delivery_note.make_shipment",
+			frm: this.frm
+		})
+	},
+
 	make_sales_invoice: function() {
 		frappe.model.open_mapped_doc({
 			method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index d04cf78..00a66fa 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -569,6 +569,59 @@
 
 	return doclist
 
+@frappe.whitelist()
+def make_shipment(source_name, target_doc=None):
+	def postprocess(source, target):
+		user = frappe.db.get_value("User", frappe.session.user, ['email', 'full_name', 'phone', 'mobile_no'], as_dict=1)
+		target.pickup_contact_email = user.email
+		pickup_contact_display = '{}'.format(user.full_name)
+		if user.email:
+			pickup_contact_display += '<br>' + user.email
+		if user.phone:
+			pickup_contact_display += '<br>' + user.phone
+		if user.mobile_no and not user.phone:
+			pickup_contact_display += '<br>' + user.mobile_no
+		target.pickup_contact = pickup_contact_display
+
+		contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1)
+		delivery_contact_display = '{}'.format(source.contact_display)
+		if contact.email_id:
+			delivery_contact_display += '<br>' + contact.email_id
+		if contact.phone:
+			delivery_contact_display += '<br>' + contact.phone
+		if contact.mobile_no and not contact.phone:
+			delivery_contact_display += '<br>' + contact.mobile_no
+		target.delivery_contact = delivery_contact_display
+
+	doclist = get_mapped_doc("Delivery Note", source_name, 	{
+		"Delivery Note": {
+			"doctype": "Shipment",
+			"field_map": {
+				"grand_total": "value_of_goods",
+				"company": "pickup_company",
+				"company_address": "pickup_address_name",
+				"company_address_display": "pickup_address",
+				"address_display": "delivery_address",
+				"customer": "delivery_customer",
+				"shipping_address_name": "delivery_address_name",
+				"contact_person": "delivery_contact_name",
+				"contact_email": "delivery_contact_email"
+			},
+			"validation": {
+				"docstatus": ["=", 1]
+			}
+		},
+		"Delivery Note Item": {
+			"doctype": "Shipment Delivery Notes",
+			"field_map": {
+				"name": "prevdoc_detail_docname",
+				"parent": "prevdoc_docname",
+				"parenttype": "prevdoc_doctype",
+			}
+		}
+	}, target_doc, postprocess)
+	
+	return doclist
 
 @frappe.whitelist()
 def make_sales_return(source_name, target_doc=None):
diff --git a/erpnext/stock/doctype/parcel_service/__init__.py b/erpnext/stock/doctype/parcel_service/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service/__init__.py
diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.js b/erpnext/stock/doctype/parcel_service/parcel_service.js
new file mode 100644
index 0000000..43b8ed5
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service/parcel_service.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Parcel Service', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.json b/erpnext/stock/doctype/parcel_service/parcel_service.json
new file mode 100644
index 0000000..9960acf
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service/parcel_service.json
@@ -0,0 +1,56 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:parcel_service_name",
+ "creation": "2020-07-23 10:35:38.211715",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "parcel_service_name",
+  "parcel_service_code",
+  "url_reference"
+ ],
+ "fields": [
+  {
+   "fieldname": "parcel_service_name",
+   "fieldtype": "Data",
+   "label": "Parcel Service Name",
+   "unique": 1
+  },
+  {
+   "fieldname": "parcel_service_code",
+   "fieldtype": "Data",
+   "label": "Parcel Service Code"
+  },
+  {
+   "fieldname": "url_reference",
+   "fieldtype": "Data",
+   "label": "URL Reference"
+  }
+ ],
+ "links": [],
+ "modified": "2020-07-23 10:35:38.211715",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Parcel Service",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.py b/erpnext/stock/doctype/parcel_service/parcel_service.py
new file mode 100644
index 0000000..e46ac76
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service/parcel_service.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ParcelService(Document):
+	pass
diff --git a/erpnext/stock/doctype/parcel_service/test_parcel_service.py b/erpnext/stock/doctype/parcel_service/test_parcel_service.py
new file mode 100644
index 0000000..c2f96d9
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service/test_parcel_service.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestParcelService(unittest.TestCase):
+	pass
diff --git a/erpnext/stock/doctype/parcel_service_type/__init__.py b/erpnext/stock/doctype/parcel_service_type/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service_type/__init__.py
diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js
new file mode 100644
index 0000000..31d5453
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js
@@ -0,0 +1,12 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Parcel Service Type Alias', {
+	parcel_type_alias: function(frm, cdt, cdn) {
+		let row = locals[cdt][cdn];
+		if (row.parcel_type_alias) {
+			frappe.model.set_value(cdt, cdn, 'parcel_service', frm.doc.parcel_service);
+			frm.refresh_field('parcel_service_type_alias');
+		}
+	}
+});
diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json
new file mode 100644
index 0000000..3c0c4d5
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json
@@ -0,0 +1,89 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format: {parcel_service} - {parcel_service_type}",
+ "creation": "2020-07-23 10:47:43.794083",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "parcel_service",
+  "parcel_service_type",
+  "description",
+  "section_break_4",
+  "parcel_service_type_alias",
+  "column_break_6",
+  "section_break_7",
+  "show_in_preferred_services_list"
+ ],
+ "fields": [
+  {
+   "fieldname": "parcel_service",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Parcel Service",
+   "options": "Parcel Service",
+   "reqd": 1
+  },
+  {
+   "fieldname": "parcel_service_type",
+   "fieldtype": "Data",
+   "label": "Parcel Service Type",
+   "reqd": 1,
+   "set_only_once": 1
+  },
+  {
+   "fieldname": "description",
+   "fieldtype": "Small Text",
+   "label": "Description"
+  },
+  {
+   "fieldname": "section_break_4",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "parcel_service_type_alias",
+   "fieldtype": "Table",
+   "label": "Parcel Service Type Alias",
+   "options": "Parcel Service Type Alias"
+  },
+  {
+   "fieldname": "column_break_6",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_7",
+   "fieldtype": "Section Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "show_in_preferred_services_list",
+   "fieldtype": "Check",
+   "label": "Show in Preferred Services List"
+  }
+ ],
+ "links": [],
+ "modified": "2020-07-23 10:47:43.794083",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Parcel Service Type",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py
new file mode 100644
index 0000000..b55528c
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class ParcelServiceType(Document):
+	pass
+
+def match_parcel_service_type_alias(parcel_service_type, parcel_service):
+	# Match and return Parcel Service Type Alias to Parcel Service Type if exists.
+	if frappe.db.exists('Parcel Service', parcel_service):
+		matched_parcel_service_type = \
+			frappe.db.get_value('Parcel Service Type Alias', {
+				'parcel_type_alias': parcel_service_type,
+				'parcel_service': parcel_service
+			}, 'parent')
+		if matched_parcel_service_type:
+			parcel_service_type = matched_parcel_service_type
+	return parcel_service_type
diff --git a/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py b/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py
new file mode 100644
index 0000000..e214264
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestParcelServiceType(unittest.TestCase):
+	pass
diff --git a/erpnext/stock/doctype/parcel_service_type_alias/__init__.py b/erpnext/stock/doctype/parcel_service_type_alias/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service_type_alias/__init__.py
diff --git a/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json
new file mode 100644
index 0000000..8e7731e
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json
@@ -0,0 +1,41 @@
+{
+ "actions": [],
+ "creation": "2020-07-23 10:47:23.626510",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "parcel_service",
+  "parcel_type_alias"
+ ],
+ "fields": [
+  {
+   "fieldname": "parcel_service",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "in_list_view": 1,
+   "label": "Parcel Service",
+   "options": "Parcel Service",
+   "read_only": 1
+  },
+  {
+   "fieldname": "parcel_type_alias",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Parcel Type Alias",
+   "reqd": 1
+  }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-23 10:47:23.626510",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Parcel Service Type Alias",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py
new file mode 100644
index 0000000..fd0a7d8
--- /dev/null
+++ b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ParcelServiceTypeAlias(Document):
+	pass
diff --git a/erpnext/stock/doctype/shipment/__init__.py b/erpnext/stock/doctype/shipment/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/shipment/__init__.py
diff --git a/erpnext/stock/doctype/shipment/api/utils.py b/erpnext/stock/doctype/shipment/api/utils.py
new file mode 100644
index 0000000..1153933
--- /dev/null
+++ b/erpnext/stock/doctype/shipment/api/utils.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+import re
+
+def get_address(address_name):
+	address = frappe.db.get_value('Address', address_name, [
+		'address_title',
+		'address_line1',
+		'address_line2',
+		'city',
+		'pincode',
+		'country',
+	], as_dict=1)
+	address.country_code = frappe.db.get_value('Country', address.country, 'code').upper()
+	if not address.pincode or address.pincode == '':
+		frappe.throw(_("Postal Code is mandatory to continue. </br> \
+			Please set Postal Code for Address <a href='#Form/Address/{0}'>{1}</a>"
+		).format(address_name, address_name))
+	address.pincode = address.pincode.replace(' ', '')
+	address.city = address.city.strip()
+	return address
+
+def get_contact(contact_name):
+	contact = frappe.db.get_value('Contact', contact_name, [
+		'first_name',
+		'last_name',
+		'email_id',
+		'phone',
+		'mobile_no',
+		'gender',
+	], as_dict=1)
+	if not contact.last_name:
+		frappe.throw(_("Last Name is mandatory to continue. </br> \
+			Please set Last Name for Contact <a href='#Form/Contact/{0}'>{1}</a>"
+		).format(contact_name, contact_name))
+	if not contact.phone:
+		contact.phone = contact.mobile_no
+	contact.phone_prefix = contact.phone[:3]
+	contact.phone = re.sub('[^A-Za-z0-9]+', '', contact.phone[3:])
+	contact.email = contact.email_id
+	contact.title = 'MS'
+	if contact.gender == 'Male':
+		contact.title = 'MR'
+	return contact
+
+def get_company_contact():
+	contact = frappe.db.get_value('User', frappe.session.user, [
+		'first_name',
+		'last_name',
+		'email',
+		'phone',
+		'mobile_no',
+		'gender',
+	], as_dict=1)
+	if not contact.phone:
+		contact.phone = contact.mobile_no
+	contact.phone_prefix = contact.phone[:3]
+	contact.phone = re.sub('[^A-Za-z0-9]+', '', contact.phone[3:])
+	contact.title = 'MS'
+	if contact.gender == 'Male':
+		contact.title = 'MR'
+	return contact
diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js
new file mode 100644
index 0000000..e9f4484
--- /dev/null
+++ b/erpnext/stock/doctype/shipment/shipment.js
@@ -0,0 +1,772 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Shipment', {
+	setup: function(frm) {
+		if (frm.doc.__islocal) {
+			frm.trigger('pickup_type');
+		}
+	},
+	address_query: function(frm, link_doctype, link_name, is_your_company_address) {
+		return {
+			query: 'frappe.contacts.doctype.address.address.address_query',
+			filters: {
+				link_doctype: link_doctype,
+				link_name: link_name,
+				is_your_company_address: is_your_company_address
+			}
+		};
+	},
+	contact_query: function(frm, link_doctype, link_name) {
+		return {
+			query: 'frappe.contacts.doctype.contact.contact.contact_query',
+			filters: {
+				link_doctype: link_doctype,
+				link_name: link_name
+			}
+		};
+	},
+	onload: function(frm) {
+		frm.set_query("delivery_address_name", () => {
+			let link_doctype = '';
+			let link_name = '';
+			let is_your_company_address = 0;
+			if (frm.doc.delivery_to_type == 'Customer') {
+				link_doctype = 'Customer';
+				link_name = frm.doc.delivery_customer;
+			}
+			if (frm.doc.delivery_to_type == 'Supplier') {
+				link_doctype = 'Supplier';
+				link_name = frm.doc.delivery_supplier;
+			}
+			if (frm.doc.delivery_to_type == 'Company') {
+				link_doctype = 'Company';
+				link_name = frm.doc.delivery_company;
+				is_your_company_address = 1;
+			}
+			return frm.events.address_query(frm, link_doctype, link_name, is_your_company_address);
+		});
+		frm.set_query("pickup_address_name", () => {
+			let link_doctype = '';
+			let link_name = '';
+			let is_your_company_address = 0;
+			if (frm.doc.pickup_from_type == 'Customer') {
+				link_doctype = 'Customer';
+				link_name = frm.doc.pickup_customer;
+			}
+			if (frm.doc.pickup_from_type == 'Supplier') {
+				link_doctype = 'Supplier';
+				link_name = frm.doc.pickup_supplier;
+			}
+			if (frm.doc.pickup_from_type == 'Company') {
+				link_doctype = 'Company';
+				link_name = frm.doc.pickup_company;
+				is_your_company_address = 1;
+			}
+			return frm.events.address_query(frm, link_doctype, link_name, is_your_company_address);
+		});
+		frm.set_query("delivery_contact_name", () => {
+			let link_doctype = '';
+			let link_name = '';
+			if (frm.doc.delivery_to_type == 'Customer') {
+				link_doctype = 'Customer';
+				link_name = frm.doc.delivery_customer;
+			}
+			if (frm.doc.delivery_to_type == 'Supplier') {
+				link_doctype = 'Supplier';
+				link_name = frm.doc.delivery_supplier;
+			}
+			if (frm.doc.delivery_to_type == 'Company') {
+				link_doctype = 'Company';
+				link_name = frm.doc.delivery_company;
+			}
+			return frm.events.contact_query(frm, link_doctype, link_name);
+		});
+		frm.set_query("pickup_contact_name", () => {
+			let link_doctype = '';
+			let link_name = '';
+			if (frm.doc.pickup_from_type == 'Customer') {
+				link_doctype = 'Customer';
+				link_name = frm.doc.pickup_customer;
+			}
+			if (frm.doc.pickup_from_type == 'Supplier') {
+				link_doctype = 'Supplier';
+				link_name = frm.doc.pickup_supplier;
+			}
+			if (frm.doc.pickup_from_type == 'Company') {
+				link_doctype = 'Company';
+				link_name = frm.doc.pickup_company;
+			}
+			return frm.events.contact_query(frm, link_doctype, link_name);
+		});
+		frm.set_query("delivery_note", "shipment_delivery_notes", function() {
+			let customer = '';
+			if (frm.doc.delivery_to_type == "Customer") {
+				customer = frm.doc.delivery_customer;
+			}
+			if (frm.doc.delivery_to_type == "Company") {
+				customer = frm.doc.delivery_company;
+			}
+			if (customer) {
+				return {
+					filters: {
+						customer: customer,
+						docstatus: 1,
+						status: ["not in", ["Cancelled"]]
+					}
+				};
+			}
+		});
+	},
+	refresh: function(frm) {
+		if (frm.doc.docstatus === 1 && !frm.doc.shipment_id) {
+			frm.add_custom_button(__('Fetch Shipping Rates'), function() {
+				return frm.events.fetch_shipping_rates(frm);
+			});
+		}
+		if (frm.doc.shipment_id) {
+			frm.add_custom_button(__('Print Shipping Label'), function() {
+				return frm.events.print_shipping_label(frm);
+			});
+			if (frm.doc.tracking_status != 'Delivered') {
+				frm.add_custom_button(__('Update Tracking'), function() {
+					return frm.events.update_tracking(frm, frm.doc.service_provider, frm.doc.shipment_id);
+				});
+			}
+		}
+		$('div[data-fieldname=pickup_address] > div > .clearfix').hide();
+		$('div[data-fieldname=pickup_contact] > div > .clearfix').hide();
+		$('div[data-fieldname=delivery_address] > div > .clearfix').hide();
+		$('div[data-fieldname=delivery_contact] > div > .clearfix').hide();
+
+		if (frm.doc.delivery_from_type != 'Company') {
+			frm.set_df_property("delivery_contact_name", "reqd", 1);
+		}
+		if (frm.doc.pickup_from_type != 'Company') {
+			frm.set_df_property("pickup_contact_name", "reqd", 1);
+		}
+		else {
+			frm.toggle_display("pickup_contact_name", false);
+		}
+	},
+	before_save: function(frm) {
+		if (frm.doc.delivery_to_type == 'Company') {
+			frm.set_value("delivery_to", frm.doc.delivery_company);
+		}
+		if (frm.doc.delivery_to_type == 'Customer') {
+			frm.set_value("delivery_to", frm.doc.delivery_customer);
+		}
+		if (frm.doc.delivery_to_type == 'Supplier') {
+			frm.set_value("delivery_to", frm.doc.delivery_supplier);
+		}
+		if (frm.doc.pickup_from_type == 'Company') {
+			frm.set_value("pickup", frm.doc.pickup_company);
+		}
+		if (frm.doc.pickup_from_type == 'Customer') {
+			frm.set_value("pickup", frm.doc.pickup_customer);
+		}
+		if (frm.doc.pickup_from_type == 'Supplier') {
+			frm.set_value("pickup", frm.doc.pickup_supplier);
+		}
+	},
+	set_pickup_company_address: function(frm) {
+		frappe.db.get_value('Address', {
+			address_title: frm.doc.pickup_company,
+			is_your_company_address: 1
+		}, 'name', (r) => {
+			frm.set_value("pickup_address_name", r.name);
+		});
+	},
+	set_delivery_company_address: function(frm) {
+		frappe.db.get_value('Address', {
+			address_title: frm.doc.delivery_company,
+			is_your_company_address: 1
+		}, 'name', (r) => {
+			frm.set_value("delivery_address_name", r.name);
+		});
+	},
+	pickup_from_type: function(frm) {
+		if (frm.doc.pickup_from_type == 'Company') {
+			frm.set_value("pickup_company", frappe.defaults.get_default('company'));
+			frm.set_df_property("pickup_contact_name", "reqd", 0);
+			frm.set_value("pickup_customer", '');
+			frm.set_value("pickup_supplier", '');
+			frm.toggle_display("pickup_contact_name", false);
+		}
+		else {
+			frm.set_df_property("pickup_contact_name", "reqd", 1);
+			frm.toggle_display("pickup_contact_name", true);
+			frm.trigger('clear_pickup_fields');
+		}
+		if (frm.doc.pickup_from_type == 'Customer') {
+			frm.set_value("pickup_company", '');
+			frm.set_value("pickup_supplier", '');
+		}
+		if (frm.doc.pickup_from_type == 'Supplier') {
+			frm.set_value("pickup_customer", '');
+			frm.set_value("pickup_company", '');
+		}
+		frm.events.remove_notific_child_table(frm, 'shipment_notification_subscriptions', 'Pickup');
+		frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscriptions', 'Pickup');
+	},
+	delivery_to_type: function(frm) {
+		if (frm.doc.delivery_to_type == 'Company') {
+			frm.set_value("delivery_company", frappe.defaults.get_default('company'));
+			frm.set_df_property("delivery_contact_name", "reqd", 0);
+			frm.set_value("delivery_customer", '');
+			frm.set_value("delivery_supplier", '');
+			frm.toggle_display("delivery_contact_name", false);
+		}
+		else {
+			frm.set_df_property("delivery_contact_name", "reqd", 1);
+			frm.toggle_display("delivery_contact_name", true);
+			frm.trigger('clear_delivery_fields');
+		}
+		if (frm.doc.delivery_to_type == 'Customer') {
+			frm.set_value("delivery_company", '');
+			frm.set_value("delivery_supplier", '');
+		}
+		if (frm.doc.delivery_to_type == 'Supplier') {
+			frm.set_value("delivery_customer", '');
+			frm.set_value("delivery_company", '');
+			frm.toggle_display("shipment_delivery_notes", false);
+		}
+		else {
+			frm.toggle_display("shipment_delivery_notes", true);
+		}
+		frm.events.remove_notific_child_table(frm, 'shipment_notification_subscriptions', 'Delivery');
+		frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscriptions', 'Delivery');
+	},
+	delivery_address_name: function(frm) {
+		if (frm.doc.delivery_to_type == 'Company') {
+			erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', true);
+		}
+		else {
+			erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', false);
+		}
+	},
+	pickup_address_name: function(frm) {
+		if (frm.doc.pickup_from_type == 'Company') {
+			erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', true);
+		}
+		else {
+			erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', false);
+		}
+	},
+	get_contact_display: function(frm, contact_name, contact_type) {
+		frappe.call({
+			method: "frappe.contacts.doctype.contact.contact.get_contact_details",
+			args: { contact: contact_name },
+			callback: function(r) {
+				if(r.message) {
+					if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) {
+						if (contact_type == 'Delivery') {
+							frm.set_value('delivery_contact_name', '');
+							frm.set_value('delivery_contact', '');
+						}
+						else {
+							frm.set_value('pickup_contact_name', '');
+							frm.set_value('pickup_contact', '');
+						}
+						frappe.throw(__(`Email or Phone/Mobile of the Contact are mandatory to continue. </br>
+							Please set Email/Phone for the contact <a href="#Form/Contact/${contact_name}">${contact_name}</a>`));
+					}
+					let contact_display = r.message.contact_display;
+					if (r.message.contact_email) {
+						contact_display += '<br>' + r.message.contact_email;
+					}
+					if (r.message.contact_phone) {
+						contact_display += '<br>' + r.message.contact_phone;
+					}
+					if (r.message.contact_mobile && !r.message.contact_phone) {
+						contact_display += '<br>' + r.message.contact_mobile;
+					}
+					if (contact_type == 'Delivery'){
+						frm.set_value('delivery_contact', contact_display);
+						if (r.message.contact_email) {
+							frm.set_value('delivery_contact_email', r.message.contact_email);
+						}
+					}
+					else {
+						frm.set_value('pickup_contact', contact_display);
+						if (r.message.contact_email) {
+							frm.set_value('pickup_contact_email', r.message.contact_email);
+						}
+					}
+				}
+			}
+		});
+	},
+	delivery_contact_name: function(frm) {
+		if (frm.doc.delivery_contact_name) {
+			frm.events.get_contact_display(frm, frm.doc.delivery_contact_name, 'Delivery');
+		}
+	},
+	pickup_contact_name: function(frm) {
+		if (frm.doc.pickup_contact_name) {
+			frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, 'Pickup');
+		}
+	},
+	set_company_contact: function(frm, delivery_type) {
+		frappe.db.get_value('User', { name: frappe.session.user }, ['full_name', 'last_name', 'email', 'phone', 'mobile_no'], (r) => {
+			if (!(r.last_name && r.email && (r.phone || r.mobile_no))) {
+				if (delivery_type == 'Delivery') {
+					frm.set_value('delivery_company', '');
+					frm.set_value('delivery_contact', '');
+				}
+				else {
+					frm.set_value('pickup_company', '');
+					frm.set_value('pickup_contact', '');
+				}
+				frappe.throw(__(`Last Name, Email or Phone/Mobile of the user are mandatory to continue. </br>
+					Please first set Last Name, Email and Phone for the user <a href="#Form/User/${frappe.session.user}">${frappe.session.user}</a>`));
+			}
+			let contact_display = r.full_name;
+			if (r.email) {
+				contact_display += '<br>' + r.email;
+			}
+			if (r.phone) {
+				contact_display += '<br>' + r.phone;
+			}
+			if (r.mobile_no && !r.phone) {
+				contact_display += '<br>' + r.mobile_no;
+			}
+			if (delivery_type == 'Delivery') {
+				frm.set_value('delivery_contact', contact_display);
+				if (r.email) {
+					frm.set_value('delivery_contact_email', r.email);
+				}
+			}
+			else {
+				frm.set_value('pickup_contact', contact_display);
+				if (r.email) {
+					frm.set_value('pickup_contact_email', r.email);
+				}
+			}
+		});
+	},
+	pickup_company: function(frm) {
+		if (frm.doc.pickup_from_type == 'Company'  && frm.doc.pickup_company) {
+			frm.trigger('set_pickup_company_address');
+			frm.events.set_company_contact(frm, 'Pickup');
+		}
+	},
+	delivery_company: function(frm) {
+		if (frm.doc.delivery_to_type == 'Company' && frm.doc.delivery_company) {
+			frm.trigger('set_delivery_company_address');
+			frm.events.set_company_contact(frm, 'Delivery');
+		}
+	},
+	delivery_customer: function(frm) {
+		frm.trigger('clear_delivery_fields');
+		if (frm.doc.delivery_customer) {
+			frm.events.set_address_name(frm,'Customer',frm.doc.delivery_customer, 'Delivery');
+			frm.events.set_contact_name(frm,'Customer',frm.doc.delivery_customer, 'Delivery');
+		}
+	},
+	delivery_supplier: function(frm) {
+		frm.trigger('clear_delivery_fields');
+		if (frm.doc.delivery_supplier) {
+			frm.events.set_address_name(frm,'Supplier',frm.doc.delivery_supplier, 'Delivery');
+			frm.events.set_contact_name(frm,'Supplier',frm.doc.delivery_supplier, 'Delivery');
+		}
+	},
+	pickup_customer: function(frm) {
+		frm.trigger('clear_pickup_fields');
+		if (frm.doc.pickup_customer) {
+			frm.events.set_address_name(frm,'Customer',frm.doc.pickup_customer, 'Pickup');
+			frm.events.set_contact_name(frm,'Customer',frm.doc.pickup_customer, 'Pickup');
+		}
+	},
+	pickup_supplier: function(frm) {
+		frm.trigger('clear_pickup_fields');
+		if (frm.doc.pickup_supplier) {
+			frm.events.set_address_name(frm,'Supplier',frm.doc.pickup_supplier, 'Pickup');
+			frm.events.set_contact_name(frm,'Supplier',frm.doc.pickup_supplier, 'Pickup');
+		}
+	},
+	set_address_name: function(frm, ref_doctype, ref_docname, delivery_type) {
+		frappe.call({
+			method: "erpnext.stock.doctype.shipment.shipment.get_address_name",
+			args: {
+				ref_doctype: ref_doctype,
+				docname: ref_docname
+			},
+			callback: function(r) {
+				if(r.message) {
+					if (delivery_type == 'Delivery') {
+						frm.set_value('delivery_address_name', r.message);
+					}
+					else {
+						frm.set_value('pickup_address_name', r.message);
+					}
+				}
+			}
+		});
+	},
+	set_contact_name: function(frm, ref_doctype, ref_docname, delivery_type) {
+		frappe.call({
+			method: "erpnext.stock.doctype.shipment.shipment.get_contact_name",
+			args: {
+				ref_doctype: ref_doctype,
+				docname: ref_docname
+			},
+			callback: function(r) {
+				if(r.message) {
+					if (delivery_type == 'Delivery') {
+						frm.set_value('delivery_contact_name', r.message);
+					}
+					else {
+						frm.set_value('pickup_contact_name', r.message);
+					}
+				}
+			}
+		});
+	},
+	add_template: function(frm) {
+		if (frm.doc.parcel_template) {
+			frappe.model.with_doc("Shipment Parcel Template", frm.doc.parcel_template, () => {
+				let parcel_template = frappe.model.get_doc("Shipment Parcel Template", frm.doc.parcel_template);
+				let row = frappe.model.add_child(frm.doc, "Shipment Parcel", "shipment_parcel");
+				row.length = parcel_template.length;
+				row.width = parcel_template.width;
+				row.height = parcel_template.height;
+				row.weight = parcel_template.weight;
+				frm.refresh_fields("shipment_parcel");
+			});
+		}
+	},
+	pickup_date: function(frm) {
+		if (frm.doc.pickup_date < frappe.datetime.get_today()) {
+			frappe.throw(__("Pickup Date cannot be in the past"));
+		}
+		if (frm.doc.pickup_date == frappe.datetime.get_today()) {
+			var pickup_time = frm.events.get_pickup_time(frm);
+			frm.set_value("pickup_from", pickup_time);
+			frm.trigger('set_pickup_to_time');
+		}
+	},
+	pickup_from: function(frm) {
+		var pickup_time = frm.events.get_pickup_time(frm);
+		if (frm.doc.pickup_from && frm.doc.pickup_date == frappe.datetime.get_today()) {
+			let current_hour = pickup_time.split(':')[0];
+			let current_min = pickup_time.split(':')[1];
+			let pickup_hour = frm.doc.pickup_from.split(':')[0];
+			let pickup_min = frm.doc.pickup_from.split(':')[1];
+			if (pickup_hour < current_hour || (pickup_hour == current_hour && pickup_min < current_min)) {
+				frm.set_value("pickup_from", pickup_time);
+				frappe.throw(__("Pickup Time cannot be in the past"));
+			}
+		}
+		frm.trigger('set_pickup_to_time');
+	},
+	get_pickup_time: function() {
+		let current_hour = new Date().getHours();
+		let current_min = new Date().toLocaleString('en-US', {minute: 'numeric'});
+		if (current_min < 30) {
+			current_min = '30';
+		}
+		else {
+			current_min = '00';
+			current_hour = Number(current_hour)+1;
+		}
+		if (Number(current_hour) > 19 || Number(current_hour) === 19){
+			frappe.throw(__("Today's pickup time is over, please select different date"));
+		}
+		current_hour = (current_hour < 10) ? '0' + current_hour : current_hour;
+		let pickup_time = current_hour +':'+ current_min;
+		return pickup_time;
+	},
+	set_pickup_to_time: function(frm) {
+		let pickup_to_hour = Number(frm.doc.pickup_from.split(':')[0])+5;
+		if (Number(pickup_to_hour) > 19 || Number(pickup_to_hour) === 19){
+			pickup_to_hour = 19;
+		}
+		let pickup_to_min = frm.doc.pickup_from.split(':')[1];
+		let pickup_to = pickup_to_hour +':'+ pickup_to_min;
+		frm.set_value("pickup_to", pickup_to);
+	},
+	clear_pickup_fields: function(frm) {
+		frm.set_value("pickup_address_name", '');
+		frm.set_value("pickup_contact_name", '');
+		frm.set_value("pickup_address", '');
+		frm.set_value("pickup_contact", '');
+		frm.set_value("pickup_contact_email", '');
+	},
+	clear_delivery_fields: function(frm) {
+		frm.set_value("delivery_address_name", '');
+		frm.set_value("delivery_contact_name", '');
+		frm.set_value("delivery_address", '');
+		frm.set_value("delivery_contact", '');
+		frm.set_value("delivery_contact_email", '');
+	},
+	pickup_from_send_shipping_notification: function(frm, cdt, cdn) {
+		if (frm.doc.pickup_contact_email && frm.doc.pickup_from_send_shipping_notification
+				&& !validate_duplicate(frm, 'shipment_notification_subscriptions', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) {
+			let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscriptions", "shipment_notification_subscriptions");
+			row.email = frm.doc.pickup_contact_email;
+			frm.refresh_fields("shipment_notification_subscriptions");
+		}
+		if (!frm.doc.pickup_from_send_shipping_notification) {
+			frm.events.remove_email_row(frm, 'shipment_notification_subscriptions', frm.doc.pickup_contact_email);
+			frm.refresh_fields("shipment_notification_subscriptions");
+		}
+	},
+	pickup_from_subscribe_to_status_updates: function(frm, cdt, cdn) {
+		if (frm.doc.pickup_contact_email && frm.doc.pickup_from_subscribe_to_status_updates
+				&& !validate_duplicate(frm, 'shipment_status_update_subscriptions', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) {
+			let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscriptions", "shipment_status_update_subscriptions");
+			row.email = frm.doc.pickup_contact_email;
+			frm.refresh_fields("shipment_status_update_subscriptions");
+		}
+		if (!frm.doc.pickup_from_subscribe_to_status_updates) {
+			frm.events.remove_email_row(frm, 'shipment_status_update_subscriptions', frm.doc.pickup_contact_email);
+			frm.refresh_fields("shipment_status_update_subscriptions");
+		}
+	},
+	delivery_to_send_shipping_notification: function(frm, cdt, cdn) {
+		if (frm.doc.delivery_contact_email && frm.doc.delivery_to_send_shipping_notification
+				&& !validate_duplicate(frm, 'shipment_notification_subscriptions', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)){
+			let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscriptions", "shipment_notification_subscriptions");
+			row.email = frm.doc.delivery_contact_email;
+			frm.refresh_fields("shipment_notification_subscriptions");
+		}
+		if (!frm.doc.delivery_to_send_shipping_notification) {
+			frm.events.remove_email_row(frm, 'shipment_notification_subscriptions', frm.doc.delivery_contact_email);
+			frm.refresh_fields("shipment_notification_subscriptions");
+		}
+	},
+	delivery_to_subscribe_to_status_updates: function(frm, cdt, cdn) {
+		if (frm.doc.delivery_contact_email && frm.doc.delivery_to_subscribe_to_status_updates
+				&& !validate_duplicate(frm, 'shipment_status_update_subscriptions', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)) {
+			let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscriptions", "shipment_status_update_subscriptions");
+			row.email = frm.doc.delivery_contact_email;
+			frm.refresh_fields("shipment_status_update_subscriptions");
+		}
+		if (!frm.doc.delivery_to_subscribe_to_status_updates) {
+			frm.events.remove_email_row(frm, 'shipment_status_update_subscriptions', frm.doc.delivery_contact_email);
+			frm.refresh_fields("shipment_status_update_subscriptions");
+		}
+	},
+	remove_email_row: function(frm, table, fieldname) {
+		$.each(frm.doc[table] || [], function(i, detail) {
+			if(detail.email === fieldname){
+				cur_frm.get_field(table).grid.grid_rows[i].remove();
+			}
+		});
+	},
+	remove_notific_child_table: function(frm, table, delivery_type) {
+		$.each(frm.doc[table] || [], function(i, detail) {
+			if (detail.email != frm.doc.pickup_email ||  detail.email != frm.doc.delivery_email){
+				cur_frm.get_field(table).grid.grid_rows[i].remove();
+			}
+		});
+		frm.refresh_fields(table);
+		if (delivery_type == 'Delivery') {
+			frm.set_value("delivery_to_send_shipping_notification", 0);
+			frm.set_value("delivery_to_subscribe_to_status_updates", 0);
+			frm.refresh_fields("delivery_to_send_shipping_notification");
+			frm.refresh_fields("delivery_to_subscribe_to_status_updates");
+		}
+		else {
+			frm.set_value("pickup_from_send_shipping_notification", 0);
+			frm.set_value("pickup_from_subscribe_to_status_updates", 0);
+			frm.refresh_fields("pickup_from_send_shipping_notification");
+			frm.refresh_fields("pickup_from_subscribe_to_status_updates");
+		}
+	},
+	fetch_shipping_rates: function(frm) {
+		if (!frm.doc.shipment_id) {
+			frappe.call({
+				method: "erpnext.stock.doctype.shipment.shipment.fetch_shipping_rates",
+				freeze: true,
+				freeze_message: __("Fetching Shipping Rates"),
+				args: {
+					pickup_from_type: frm.doc.pickup_from_type,
+					delivery_to_type: frm.doc.delivery_to_type,
+					pickup_address_name: frm.doc.pickup_address_name,
+					delivery_address_name: frm.doc.delivery_address_name,
+					shipment_parcel: frm.doc.shipment_parcel,
+					description_of_content: frm.doc.description_of_content,
+					pickup_date: frm.doc.pickup_date,
+					pickup_contact_name: frm.doc.pickup_contact_name,
+					delivery_contact_name: frm.doc.delivery_contact_name,
+					value_of_goods: frm.doc.value_of_goods
+				},
+				callback: function(r) {
+					if (r.message) {
+						select_from_available_services(frm, r.message);
+					}
+					else {
+						frappe.throw(__("No Shipment Services available"));
+					}
+				}
+			});
+		}
+		else {
+			frappe.throw(__("Shipment already created"));
+		}
+	},
+	print_shipping_label: function(frm) {
+		frappe.call({
+			method: "erpnext.stock.doctype.shipment.shipment.print_shipping_label",
+			freeze: true,
+			freeze_message: __("Printing Shipping Label"),
+			args: {
+				shipment_id: frm.doc.shipment_id,
+				service_provider: frm.doc.service_provider
+			},
+			callback: function(r) {
+				if (r.message) {
+					if (frm.doc.service_provider == "LetMeShip") {
+						var array = JSON.parse(r.message);
+						// Uint8Array for unsigned bytes
+						array = new Uint8Array(array);
+						const file = new Blob([array], {type: "application/pdf"});
+						const file_url = URL.createObjectURL(file);
+						window.open(file_url);
+					}
+					else {
+						if (Array.isArray(r.message)) {
+							r.message.forEach(url => window.open(url));
+						} else {
+							window.open(r.message);
+						}
+					}
+				}
+			}
+		});
+	},
+	update_tracking: function(frm, service_provider, shipment_id) {
+		let delivery_notes = [];
+		(frm.doc.shipment_delivery_notes || []).forEach((d) => {
+			delivery_notes.push(d.delivery_note);
+		});
+		frappe.call({
+			method: "erpnext.stock.doctype.shipment.shipment.update_tracking",
+			freeze: true,
+			freeze_message: __("Updating Tracking"),
+			args: {
+				shipment: frm.doc.name,
+				shipment_id: shipment_id,
+				service_provider: service_provider,
+				delivery_notes: delivery_notes
+			},
+			callback: function(r) {
+				if (!r.exc) {
+					frm.reload_doc();
+				}
+			}
+		});
+	}
+});
+
+frappe.ui.form.on('Shipment Delivery Notes', {
+	delivery_note: function(frm, cdt, cdn) {
+		let row = locals[cdt][cdn];
+		if (row.delivery_note) {
+			let row_index = row.idx - 1;
+			if(validate_duplicate(frm, 'shipment_delivery_notes', row.delivery_note, row_index)) {
+				cur_frm.get_field('shipment_delivery_notes').grid.grid_rows[row_index].remove();
+				frappe.throw(__(`You have entered duplicate Delivery Notes. Please rectify and try again.`));
+			}
+		}
+	},
+	grand_total: function(frm, cdt, cdn) {
+		let row = locals[cdt][cdn];
+		if (row.grand_total) {
+			var value_of_goods = parseFloat(frm.doc.value_of_goods)+parseFloat(row.grand_total);
+			frm.set_value("value_of_goods", Math.round(value_of_goods));
+			frm.refresh_fields("value_of_goods");
+		}
+	},
+});
+
+var validate_duplicate =  function(frm, table, fieldname, index){
+	let duplicate = false;
+	$.each(frm.doc[table], function(i, detail) {
+		// Email duplicate validation
+		if(detail.email === fieldname && !(index === i)) {
+			duplicate = true;
+			return;
+		}
+
+		// Delivery Note duplicate validation
+		if(detail.delivery_note === fieldname && !(index === i)) {
+			duplicate = true;
+			return;
+		}
+	});
+	return duplicate;
+};
+
+function select_from_available_services(frm, available_services) {
+	var headers = [ __("Service Provider"), __("Carrier"), __("Carrier’s Service"), __("Price"), "" ];
+	cur_frm.render_available_services = function(d, headers, data){
+		d.fields_dict.available_services.$wrapper.html(
+			frappe.render_template('shipment_service_selector',
+				{'header_columns': headers, 'data': data}
+			)
+		);
+	};
+	const d = new frappe.ui.Dialog({
+		title: __("Select Shipment Service to create Shipment"),
+		fields: [
+			{
+				fieldtype:'HTML',
+				fieldname:"available_services",
+				label: __('Available Services')
+			}
+		]
+	});
+	cur_frm.render_available_services(d, headers, available_services);
+	let shipment_notific_email = [];
+	let tracking_notific_email = [];
+	(frm.doc.shipment_notification_subscriptions || []).forEach((d) => {
+		if (!d.unsubscribed) {
+			shipment_notific_email.push(d.email);
+		}
+	});
+	(frm.doc.shipment_status_update_subscriptions || []).forEach((d) => {
+		if (!d.unsubscribed) {
+			tracking_notific_email.push(d.email);
+		}
+	});
+	let delivery_notes = [];
+	(frm.doc.shipment_delivery_notes || []).forEach((d) => {
+		delivery_notes.push(d.delivery_note);
+	});
+	cur_frm.select_row = function(service_data){
+		frappe.call({
+			method: "erpnext.stock.doctype.shipment.shipment.create_shipment",
+			freeze: true,
+			freeze_message: __("Creating Shipment"),
+			args: {
+				shipment: frm.doc.name,
+				pickup_from_type: frm.doc.pickup_from_type,
+				delivery_to_type: frm.doc.delivery_to_type,
+				pickup_address_name: frm.doc.pickup_address_name,
+				delivery_address_name: frm.doc.delivery_address_name,
+				shipment_parcel: frm.doc.shipment_parcel,
+				description_of_content: frm.doc.description_of_content,
+				pickup_date: frm.doc.pickup_date,
+				pickup_contact_name: frm.doc.pickup_contact_name,
+				delivery_contact_name: frm.doc.delivery_contact_name,
+				value_of_goods: frm.doc.value_of_goods,
+				service_data: service_data,
+				shipment_notific_email: shipment_notific_email,
+				tracking_notific_email: tracking_notific_email,
+				delivery_notes: delivery_notes
+			},
+			callback: function(r) {
+				if (!r.exc) {
+					frm.reload_doc();
+					frappe.msgprint(__("Shipment created with {0}, ID is {1}", [r.message.service_provider, r.message.shipment_id]));
+					frm.events.update_tracking(frm, r.message.service_provider, r.message.shipment_id);
+				}
+			}
+		});
+		d.hide();
+	};
+	d.show();
+}
diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json
new file mode 100644
index 0000000..b6656a2
--- /dev/null
+++ b/erpnext/stock/doctype/shipment/shipment.json
@@ -0,0 +1,478 @@
+{
+ "actions": [],
+ "autoname": "SHIPMENT-.#####",
+ "creation": "2020-07-09 10:58:52.508703",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "heading_pickup_from",
+  "pickup_from_type",
+  "pickup_company",
+  "pickup_customer",
+  "pickup_supplier",
+  "pickup",
+  "pickup_address_name",
+  "pickup_address",
+  "pickup_contact_name",
+  "pickup_contact_email",
+  "pickup_contact",
+  "column_break_2",
+  "heading_delivery_to",
+  "delivery_to_type",
+  "delivery_company",
+  "delivery_customer",
+  "delivery_supplier",
+  "delivery_to",
+  "delivery_address_name",
+  "delivery_address",
+  "delivery_contact_name",
+  "delivery_contact_email",
+  "delivery_contact",
+  "notification_details_section",
+  "pickup_from_send_shipping_notification",
+  "pickup_from_subscribe_to_status_updates",
+  "shipment_notification_subscriptions",
+  "column_break_27",
+  "delivery_to_send_shipping_notification",
+  "delivery_to_subscribe_to_status_updates",
+  "shipment_status_update_subscriptions",
+  "parcels_section",
+  "shipment_parcel",
+  "parcel_template",
+  "add_template",
+  "column_break_28",
+  "shipment_delivery_notes",
+  "shipment_details_section",
+  "pallets",
+  "value_of_goods",
+  "pickup_date",
+  "pickup_from",
+  "pickup_to",
+  "column_break_36",
+  "shipment_type",
+  "pickup_type",
+  "incoterm",
+  "description_of_content",
+  "section_break_40",
+  "shipment_information_section",
+  "service_provider",
+  "shipment_id",
+  "shipment_amount",
+  "status",
+  "tracking_url",
+  "column_break_55",
+  "carrier",
+  "carrier_service",
+  "awb_number",
+  "tracking_status",
+  "tracking_status_info",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "heading_pickup_from",
+   "fieldtype": "Heading",
+   "label": "Pickup from"
+  },
+  {
+   "default": "Company",
+   "fieldname": "pickup_from_type",
+   "fieldtype": "Select",
+   "label": "Pickup from",
+   "options": "Company\nCustomer\nSupplier"
+  },
+  {
+   "depends_on": "eval:doc.pickup_from_type == 'Company'",
+   "fieldname": "pickup_company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  },
+  {
+   "depends_on": "eval:doc.pickup_from_type == 'Customer'",
+   "fieldname": "pickup_customer",
+   "fieldtype": "Link",
+   "label": "Customer",
+   "options": "Customer"
+  },
+  {
+   "depends_on": "eval:doc.pickup_from_type == 'Supplier'",
+   "fieldname": "pickup_supplier",
+   "fieldtype": "Link",
+   "label": "Supplier",
+   "options": "Supplier"
+  },
+  {
+   "fieldname": "pickup",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "in_list_view": 1,
+   "label": "Pickup From",
+   "read_only": 1
+  },
+  {
+   "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type == \"Company\"",
+   "fieldname": "pickup_address_name",
+   "fieldtype": "Link",
+   "label": "Address",
+   "options": "Address",
+   "reqd": 1
+  },
+  {
+   "fieldname": "pickup_address",
+   "fieldtype": "Small Text",
+   "read_only": 1
+  },
+  {
+   "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type == \"Company\"",
+   "fieldname": "pickup_contact_name",
+   "fieldtype": "Link",
+   "label": "Contact",
+   "options": "Contact"
+  },
+  {
+   "fieldname": "pickup_contact_email",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Contact Email",
+   "read_only": 1
+  },
+  {
+   "fieldname": "pickup_contact",
+   "fieldtype": "Small Text",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "heading_delivery_to",
+   "fieldtype": "Heading",
+   "label": "Delivery to"
+  },
+  {
+   "default": "Customer",
+   "fieldname": "delivery_to_type",
+   "fieldtype": "Select",
+   "label": "Delivery to",
+   "options": "Company\nCustomer\nSupplier"
+  },
+  {
+   "depends_on": "eval:doc.delivery_to_type == 'Company'",
+   "fieldname": "delivery_company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  },
+  {
+   "depends_on": "eval:doc.delivery_to_type == 'Customer'",
+   "fieldname": "delivery_customer",
+   "fieldtype": "Link",
+   "label": "Customer",
+   "options": "Customer"
+  },
+  {
+   "depends_on": "eval:doc.delivery_to_type == 'Supplier'",
+   "fieldname": "delivery_supplier",
+   "fieldtype": "Link",
+   "label": "Supplier",
+   "options": "Supplier"
+  },
+  {
+   "fieldname": "delivery_to",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "in_list_view": 1,
+   "label": "Delivery To",
+   "read_only": 1
+  },
+  {
+   "depends_on": "eval: doc.delivery_customer || doc.delivery_supplier || doc.delivery_to_type == \"Company\"",
+   "fieldname": "delivery_address_name",
+   "fieldtype": "Link",
+   "label": "Address",
+   "options": "Address",
+   "reqd": 1
+  },
+  {
+   "fieldname": "delivery_address",
+   "fieldtype": "Small Text",
+   "read_only": 1
+  },
+  {
+   "depends_on": "eval: doc.delivery_customer || doc.delivery_supplier || doc.delivery_to_type == \"Company\"",
+   "fieldname": "delivery_contact_name",
+   "fieldtype": "Link",
+   "label": "Contact",
+   "options": "Contact"
+  },
+  {
+   "fieldname": "delivery_contact_email",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Contact Email",
+   "read_only": 1
+  },
+  {
+   "depends_on": "eval:doc.delivery_contact_name",
+   "fieldname": "delivery_contact",
+   "fieldtype": "Small Text",
+   "read_only": 1
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "notification_details_section",
+   "fieldtype": "Section Break",
+   "label": "Notification Details"
+  },
+  {
+   "default": "0",
+   "fieldname": "pickup_from_send_shipping_notification",
+   "fieldtype": "Check",
+   "label": "Send shipping notification"
+  },
+  {
+   "default": "0",
+   "fieldname": "pickup_from_subscribe_to_status_updates",
+   "fieldtype": "Check",
+   "label": "Subscribe to status updates"
+  },
+  {
+   "fieldname": "shipment_notification_subscriptions",
+   "fieldtype": "Table",
+   "label": "Shipment Notification Subscriptions",
+   "options": "Shipment Notification Subscriptions"
+  },
+  {
+   "fieldname": "column_break_27",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "delivery_to_send_shipping_notification",
+   "fieldtype": "Check",
+   "label": "Send shipping notification"
+  },
+  {
+   "default": "0",
+   "fieldname": "delivery_to_subscribe_to_status_updates",
+   "fieldtype": "Check",
+   "label": "Subscribe to status updates"
+  },
+  {
+   "fieldname": "shipment_status_update_subscriptions",
+   "fieldtype": "Table",
+   "label": "Shipment Status Update Subscriptions",
+   "options": "Shipment Status Update Subscriptions"
+  },
+  {
+   "fieldname": "parcels_section",
+   "fieldtype": "Section Break",
+   "label": "Parcels"
+  },
+  {
+   "fieldname": "shipment_parcel",
+   "fieldtype": "Table",
+   "label": "Shipment Parcel",
+   "options": "Shipment Parcel"
+  },
+  {
+   "fieldname": "parcel_template",
+   "fieldtype": "Link",
+   "label": "Parcel Template",
+   "options": "Shipment Parcel Template"
+  },
+  {
+   "fieldname": "add_template",
+   "fieldtype": "Button",
+   "label": "Add Template"
+  },
+  {
+   "fieldname": "column_break_28",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "shipment_delivery_notes",
+   "fieldtype": "Table",
+   "label": "Shipment Delivery Notes",
+   "options": "Shipment Delivery Notes"
+  },
+  {
+   "fieldname": "shipment_details_section",
+   "fieldtype": "Section Break",
+   "label": "Shipment details"
+  },
+  {
+   "default": "No",
+   "fieldname": "pallets",
+   "fieldtype": "Select",
+   "label": "Pallets",
+   "options": "No\nYes"
+  },
+  {
+   "fieldname": "value_of_goods",
+   "fieldtype": "Currency",
+   "label": "Value of Goods",
+   "precision": "2",
+   "reqd": 1
+  },
+  {
+   "fieldname": "pickup_date",
+   "fieldtype": "Date",
+   "in_list_view": 1,
+   "label": "Pickup Date",
+   "reqd": 1
+  },
+  {
+   "default": "09:00",
+   "fieldname": "pickup_from",
+   "fieldtype": "Select",
+   "label": "Pickup from",
+   "options": "09:00\n09:30\n10:00\n10:30\n11:00\n11:30\n12:00\n12:30\n13:00\n13:30\n14:00\n14:30\n15:00\n15:30\n16:00\n16:30\n17:00\n17:30\n18:00\n18:30\n19:00"
+  },
+  {
+   "default": "17:00",
+   "fieldname": "pickup_to",
+   "fieldtype": "Select",
+   "label": "Pickup to",
+   "options": "09:00\n09:30\n10:00\n10:30\n11:00\n11:30\n12:00\n12:30\n13:00\n13:30\n14:00\n14:30\n15:00\n15:30\n16:00\n16:30\n17:00\n17:30\n18:00\n18:30\n19:00"
+  },
+  {
+   "fieldname": "column_break_36",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "Goods",
+   "fieldname": "shipment_type",
+   "fieldtype": "Select",
+   "label": "Shipment Type",
+   "options": "Goods\nDocuments"
+  },
+  {
+   "default": "Pickup",
+   "fieldname": "pickup_type",
+   "fieldtype": "Select",
+   "label": "Pickup Type",
+   "options": "Pickup\nSelf delivery"
+  },
+  {
+   "fieldname": "description_of_content",
+   "fieldtype": "Small Text",
+   "label": "Description of Content",
+   "reqd": 1
+  },
+  {
+   "fieldname": "section_break_40",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "shipment_information_section",
+   "fieldtype": "Section Break",
+   "label": "Shipment Information"
+  },
+  {
+   "fieldname": "service_provider",
+   "fieldtype": "Read Only",
+   "label": "Service Provider"
+  },
+  {
+   "fieldname": "shipment_id",
+   "fieldtype": "Read Only",
+   "label": "Shipment ID"
+  },
+  {
+   "fieldname": "shipment_amount",
+   "fieldtype": "Currency",
+   "label": "Shipment Amount",
+   "precision": "2",
+   "read_only": 1
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "label": "Status",
+   "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted",
+   "read_only": 1
+  },
+  {
+   "fieldname": "tracking_url",
+   "fieldtype": "Small Text",
+   "label": "Tracking URL",
+   "read_only": 1
+  },
+  {
+   "fieldname": "carrier",
+   "fieldtype": "Read Only",
+   "label": "Carrier"
+  },
+  {
+   "fieldname": "carrier_service",
+   "fieldtype": "Read Only",
+   "label": "Carrier Service"
+  },
+  {
+   "fieldname": "awb_number",
+   "fieldtype": "Read Only",
+   "label": "AWB Number"
+  },
+  {
+   "fieldname": "tracking_status",
+   "fieldtype": "Select",
+   "label": "Tracking Status",
+   "options": "\nIn Progress\nDelivered\nReturned\nLost",
+   "read_only": 1
+  },
+  {
+   "fieldname": "tracking_status_info",
+   "fieldtype": "Data",
+   "label": "Tracking Status Info",
+   "read_only": 1
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Shipment",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_55",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "incoterm",
+   "fieldtype": "Select",
+   "label": "Incoterm",
+   "options": "EXW (Ex Works)\nFCA (Free Carrier)\nCPT (Carriage Paid To)\nCIP (Carriage and Insurance Paid to)\nDPU (Delivered At Place Unloaded)\nDAP (Delivered At Place)\nDDP (Delivered Duty Paid)"
+  }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-07-24 11:44:30.904612",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Shipment",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py
new file mode 100644
index 0000000..e059bac
--- /dev/null
+++ b/erpnext/stock/doctype/shipment/shipment.py
@@ -0,0 +1,300 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from frappe import _
+from frappe.model.document import Document
+from erpnext.accounts.party import get_party_shipping_address
+from frappe.contacts.doctype.contact.contact import get_default_contact
+from erpnext.erpnext_integrations.doctype.letmeship.letmeship import LETMESHIP_PROVIDER, get_letmeship_available_services, create_letmeship_shipment, get_letmeship_label, get_letmeship_tracking_data
+from erpnext.erpnext_integrations.doctype.packlink.packlink import PACKLINK_PROVIDER, get_packlink_available_services, create_packlink_shipment, get_packlink_label, get_packlink_tracking_data
+from erpnext.erpnext_integrations.doctype.sendcloud.sendcloud import SENDCLOUD_PROVIDER, get_sendcloud_available_services, create_sendcloud_shipment, get_sendcloud_label, get_sendcloud_tracking_data
+from erpnext.stock.doctype.parcel_service_type.parcel_service_type import match_parcel_service_type_alias
+
+class Shipment(Document):
+	def validate(self):
+		self.validate_weight()
+		if self.docstatus == 0:
+			self.status = 'Draft'
+
+	def on_submit(self):
+		if not self.shipment_parcel:
+			frappe.throw(_('Please enter Shipment Parcel information'))
+		if self.value_of_goods == 0:
+			frappe.throw(_('Value of goods cannot be 0'))
+		self.status = 'Submitted'
+
+	def on_cancel(self):
+		self.status = 'Cancelled'
+
+	def validate_weight(self):
+		for parcel in self.shipment_parcel:
+			if parcel.weight <= 0:
+				frappe.throw(_('Parcel weight cannot be 0'))
+
+@frappe.whitelist()
+def fetch_shipping_rates(pickup_from_type, delivery_to_type, pickup_address_name, delivery_address_name,
+		shipment_parcel, description_of_content, pickup_date, value_of_goods,
+		pickup_contact_name=None, delivery_contact_name=None):
+	# Return Shipping Rates for the various Shipping Providers
+	shipment_prices = []
+	letmeship_enabled = frappe.db.get_single_value('LetMeShip','enabled')
+	packlink_enabled = frappe.db.get_single_value('Packlink','enabled')
+	sendcloud_enabled = frappe.db.get_single_value('SendCloud','enabled')
+	pickup_address = get_address(pickup_address_name)
+	delivery_address = get_address(delivery_address_name)
+	if letmeship_enabled:
+		pickup_contact = None
+		delivery_contact = None
+		if pickup_from_type != 'Company':
+			pickup_contact = get_contact(pickup_contact_name)
+		else:
+			pickup_contact = get_company_contact()
+		
+		if delivery_to_type != 'Company':
+			delivery_contact = get_contact(delivery_contact_name)
+		else:
+			delivery_contact = get_company_contact()
+		letmeship_prices = get_letmeship_available_services(
+			delivery_to_type=delivery_to_type,
+			pickup_address=pickup_address,
+			delivery_address=delivery_address,
+			shipment_parcel=shipment_parcel,
+			description_of_content=description_of_content,
+			pickup_date=pickup_date,
+			value_of_goods=value_of_goods,
+			pickup_contact=pickup_contact,
+			delivery_contact=delivery_contact,
+		)
+		letmeship_prices = match_parcel_service_type_carrier(letmeship_prices, ['carrier', 'carrier_name'])
+		shipment_prices = shipment_prices + letmeship_prices
+	if packlink_enabled:
+		packlink_prices = get_packlink_available_services(
+			pickup_address=pickup_address,
+			delivery_address=delivery_address,
+			shipment_parcel=shipment_parcel, 
+			pickup_date=pickup_date
+		)
+		packlink_prices = match_parcel_service_type_carrier(packlink_prices, ['carrier_name', 'carrier'])
+		shipment_prices = shipment_prices + packlink_prices
+	if sendcloud_enabled and pickup_from_type == 'Company':
+		sendcloud_prices = get_sendcloud_available_services(
+			delivery_address=delivery_address,
+			shipment_parcel=shipment_parcel
+		)
+		shipment_prices = shipment_prices + sendcloud_prices
+	shipment_prices = sorted(shipment_prices, key=lambda k:k['total_price'])
+	return shipment_prices
+
+@frappe.whitelist()
+def create_shipment(shipment, pickup_from_type, delivery_to_type, pickup_address_name,
+		delivery_address_name, shipment_parcel, description_of_content, pickup_date,
+		value_of_goods, service_data, shipment_notific_email, tracking_notific_email,
+		pickup_contact_name=None, delivery_contact_name=None, delivery_notes=[]):
+	# Create Shipment for the selected provider
+	service_info = json.loads(service_data)
+	shipment_info = None
+	pickup_contact = None
+	delivery_contact = None
+	pickup_address = get_address(pickup_address_name)
+	delivery_address = get_address(delivery_address_name)
+	if pickup_from_type != 'Company':
+		pickup_contact = get_contact(pickup_contact_name)
+	else:
+		pickup_contact = get_company_contact()
+	
+	if delivery_to_type != 'Company':
+		delivery_contact = get_contact(delivery_contact_name)
+	else:
+		delivery_contact = get_company_contact()
+	if service_info['service_provider'] == LETMESHIP_PROVIDER:
+		shipment_info = create_letmeship_shipment(
+			pickup_address=pickup_address,
+			delivery_address=delivery_address,
+			shipment_parcel=shipment_parcel,
+			description_of_content=description_of_content,
+			pickup_date=pickup_date,
+			value_of_goods=value_of_goods,
+			pickup_contact=pickup_contact,
+			delivery_contact=delivery_contact,
+			service_info=service_info,
+			shipment_notific_email=shipment_notific_email,
+			tracking_notific_email=tracking_notific_email,
+		)
+
+	if service_info['service_provider'] == PACKLINK_PROVIDER:
+		shipment_info = create_packlink_shipment(
+			pickup_address=pickup_address,
+			delivery_address=delivery_address,
+			shipment_parcel=shipment_parcel,
+			description_of_content=description_of_content,
+			pickup_date=pickup_date,
+			value_of_goods=value_of_goods,
+			pickup_contact=pickup_contact,
+			delivery_contact=delivery_contact,
+			service_info=service_info,
+		)
+
+	if service_info['service_provider'] == SENDCLOUD_PROVIDER:
+		shipment_info = create_sendcloud_shipment(
+			shipment=shipment,
+			delivery_address=delivery_address,
+			shipment_parcel=shipment_parcel,
+			description_of_content=description_of_content,
+			value_of_goods=value_of_goods,
+			delivery_contact=delivery_contact,
+			service_info=service_info,
+		)
+
+	if shipment_info:
+		frappe.db.set_value('Shipment', shipment, 'service_provider', shipment_info.get('service_provider'))
+		frappe.db.set_value('Shipment', shipment, 'carrier', shipment_info.get('carrier'))
+		frappe.db.set_value('Shipment', shipment, 'carrier_service', shipment_info.get('carrier_service'))
+		frappe.db.set_value('Shipment', shipment, 'shipment_id', shipment_info.get('shipment_id'))
+		frappe.db.set_value('Shipment', shipment, 'shipment_amount', shipment_info.get('shipment_amount'))
+		frappe.db.set_value('Shipment', shipment, 'awb_number', shipment_info.get('awb_number'))
+		frappe.db.set_value('Shipment', shipment, 'status', 'Booked')
+		if delivery_notes:
+			update_delivery_note(delivery_notes=delivery_notes, shipment_info=shipment_info)
+	return shipment_info
+
+
+@frappe.whitelist()
+def print_shipping_label(service_provider, shipment_id):
+	if service_provider == LETMESHIP_PROVIDER:
+		shipping_label = get_letmeship_label(shipment_id)
+	elif service_provider == PACKLINK_PROVIDER:
+		shipping_label = get_packlink_label(shipment_id)
+	elif service_provider == SENDCLOUD_PROVIDER:
+		shipping_label = get_sendcloud_label(shipment_id)
+	return shipping_label
+
+
+@frappe.whitelist()
+def update_tracking(shipment, service_provider, shipment_id, delivery_notes=[]):
+	# Update Tracking info in Shipment
+	tracking_data = None
+	if service_provider == LETMESHIP_PROVIDER:
+		tracking_data = get_letmeship_tracking_data(shipment_id)
+	elif service_provider == PACKLINK_PROVIDER:
+		tracking_data = get_packlink_tracking_data(shipment_id)
+	elif service_provider == SENDCLOUD_PROVIDER:
+		tracking_data = get_sendcloud_tracking_data(shipment_id)
+	if tracking_data:
+		if delivery_notes:
+			update_delivery_note(delivery_notes=delivery_notes, tracking_info=tracking_data)
+		frappe.db.set_value('Shipment', shipment, 'awb_number', tracking_data.get('awb_number'))
+		frappe.db.set_value('Shipment', shipment, 'tracking_status', tracking_data.get('tracking_status'))
+		frappe.db.set_value('Shipment', shipment, 'tracking_status_info', tracking_data.get('tracking_status_info'))
+		frappe.db.set_value('Shipment', shipment, 'tracking_url', tracking_data.get('tracking_url'))
+
+@frappe.whitelist()
+def get_address_name(ref_doctype, docname):
+	# Return address name
+	return get_party_shipping_address(ref_doctype, docname)
+
+@frappe.whitelist()
+def get_contact_name(ref_doctype, docname):
+	# Return address name
+	return get_default_contact(ref_doctype, docname)
+
+def update_delivery_note(delivery_notes, shipment_info=None, tracking_info=None):
+	# Update Shipment Info in Delivery Note
+	# Using db_set since some services might not exist
+	for delivery_note in json.loads(delivery_notes):
+		dl_doc = frappe.get_doc('Delivery Note', delivery_note)
+		if shipment_info:
+			dl_doc.db_set('delivery_type', 'Parcel Service')
+			dl_doc.db_set('parcel_service', shipment_info.get('carrier'))
+			dl_doc.db_set('parcel_service_type', shipment_info.get('carrier_service'))
+		if tracking_info:		
+			dl_doc.db_set('tracking_number', tracking_info.get('awb_number'))
+			dl_doc.db_set('tracking_url', tracking_info.get('tracking_url'))
+			dl_doc.db_set('tracking_status', tracking_info.get('tracking_status'))
+			dl_doc.db_set('tracking_status_info', tracking_info.get('tracking_status_info'))
+
+
+def update_tracking_info():
+	# Daily scheduled event to update Tracking info for not delivered Shipments
+	# Also Updates the related Delivery Notes
+	shipments = frappe.get_all('Shipment', filters={
+		'docstatus': 1,
+		'status': 'Booked',
+		'shipment_id': ['!=', ''],
+		'tracking_status': ['!=', 'Delivered'],
+	})
+	for shipment in shipments:
+		shipment_doc = frappe.get_doc('Shipment', shipment.name)
+		tracking_info = \
+			update_tracking(
+				shipment_doc.service_provider,
+				shipment_doc.shipment_id,
+				shipment_doc.shipment_delivery_notes
+			)
+		if tracking_info:
+			shipment_doc.db_set('awb_number', tracking_info.get('awb_number'))
+			shipment_doc.db_set('tracking_url', tracking_info.get('tracking_url'))
+			shipment_doc.db_set('tracking_status', tracking_info.get('tracking_status'))
+			shipment_doc.db_set('tracking_status_info', tracking_info.get('tracking_status_info'))
+
+
+def get_address(address_name):
+	address = frappe.db.get_value('Address', address_name, [
+		'address_title',
+		'address_line1',
+		'address_line2',
+		'city',
+		'pincode',
+		'country',
+	], as_dict=1)
+	address.country_code = frappe.db.get_value('Country', address.country, 'code').upper()
+	if not address.pincode or address.pincode == '':
+		frappe.throw(_("Postal Code is mandatory to continue. </br> \
+				Please set Postal Code for Address <a href='#Form/Address/{0}'>{1}</a>"
+			).format(address_name, address_name))
+	address.pincode = address.pincode.replace(' ', '')
+	address.city = address.city.strip()
+	return address
+
+
+def get_contact(contact_name):
+	contact = frappe.db.get_value('Contact', contact_name, [
+		'first_name',
+		'last_name',
+		'email_id',
+		'phone',
+		'mobile_no',
+		'gender',
+	], as_dict=1)
+	if not contact.last_name:
+		frappe.throw(_("Last Name is mandatory to continue. </br> \
+				Please set Last Name for Contact <a href='#Form/Contact/{0}'>{1}</a>"
+			).format(contact_name, contact_name))
+	if not contact.phone:
+		contact.phone = contact.mobile_no
+	return contact
+
+
+def get_company_contact():
+	contact = frappe.db.get_value('User', frappe.session.user, [
+		'first_name',
+		'last_name',
+		'email',
+		'phone',
+		'mobile_no',
+		'gender',
+	], as_dict=1)
+	if not contact.phone:
+		contact.phone = contact.mobile_no
+	return contact
+
+def match_parcel_service_type_carrier(shipment_prices, reference):
+	for idx, prices in enumerate(shipment_prices):
+		service_name = match_parcel_service_type_alias(prices.get(reference[0]), prices.get(reference[1]))
+		is_preferred = frappe.db.get_value('Parcel Service Type', service_name, 'show_in_preferred_services_list')
+		shipment_prices[idx].service_name = service_name
+		shipment_prices[idx].is_preferred = is_preferred
+	return shipment_prices
diff --git a/erpnext/stock/doctype/shipment/shipment_list.js b/erpnext/stock/doctype/shipment/shipment_list.js
new file mode 100644
index 0000000..57e9209
--- /dev/null
+++ b/erpnext/stock/doctype/shipment/shipment_list.js
@@ -0,0 +1,8 @@
+frappe.listview_settings['Shipment'] = {
+	add_fields: ["status"],
+	get_indicator: function(doc) {
+		if(doc.status=='Booked') {
+			return [__("Booked"), "green"];
+		}
+	}
+};
\ No newline at end of file
diff --git a/erpnext/stock/doctype/shipment/shipment_service_selector.html b/erpnext/stock/doctype/shipment/shipment_service_selector.html
new file mode 100644
index 0000000..ed9b8bf
--- /dev/null
+++ b/erpnext/stock/doctype/shipment/shipment_service_selector.html
@@ -0,0 +1,70 @@
+{% if (data.length) { %}
+	<div style="overflow-x:scroll;height: 550px;">
+		<h5>{{ __("Preferred Services") }}</h5>
+		<table class="table table-bordered table-hover">
+			<thead class="grid-heading-row">
+				<tr style="text-align: center">
+					{% for (var i = 0; i < header_columns.length; i++) { %} 
+						<th style="text-align: center">{{ header_columns[i] }}</th>
+					{% } %}
+				</tr>
+			</thead>
+			<tbody>
+				{% for (var i = 0; i < data.length; i++) { %} 
+					{% if (data[i].is_preferred) { %} 
+						<tr style="text-align: center" id="data-list-{{i}}">
+							<td style="width:20%">{{ data[i].service_provider }}</td>
+							<td style="width:20%">{{ data[i].carrier }}</td>
+							<td style="width:40%">{{ data[i].service_name }}</td>
+							<td style="width:10%">{{ format_currency(data[i].total_price, 'EUR', 2) }}</td>
+							<td style="width:10%">
+								<button id="data-list-{{i}}" onclick='cur_frm.select_row({{JSON.stringify(data[i])}})' type="button" class="btn btn-success">
+									<span class="glyphicon glyphicon-check"></span>
+								</button>
+							</td>
+						</tr>
+					{% } %}
+				{% } %}
+			</tbody>
+		</table>
+		<h5>{{ __("Other Services") }}</h5>
+		<table class="table table-bordered table-hover">
+			<thead class="grid-heading-row">
+				<tr style="text-align: center">
+					{% for (var i = 0; i < header_columns.length; i++) { %}
+						<th style="text-align: center" >{{ header_columns[i] }}</th>
+					{% } %}
+				</tr>
+			</thead>
+			<tbody>
+				{% for (var i = 0; i < data.length; i++) { %}
+					{% if (!data[i].is_preferred) { %}
+						<tr style="text-align: center" id="data-list-{{i}}">
+							<td style="width:20%">{{ data[i].service_provider }}</td>
+							<td style="width:20%">{{ data[i].carrier }}</td>
+							<td style="width:40%">{{ data[i].service_name }}</td>
+							<td style="width:10%">{{ format_currency(data[i].total_price, 'EUR', 2) }}</td>
+							<td style="width:10%">
+								<button id="data-list-{{i}}" onclick='cur_frm.select_row({{JSON.stringify(data[i])}})' type="button" class="btn btn-success">
+									<span class="glyphicon glyphicon-check"></span>
+								</button>
+							</td>
+						</tr>
+					{% } %}
+				{% } %}
+			</tbody>
+		</table>
+	</div>
+{% } else { %}
+	<div><strong class="text-muted">{{ __("No Services Available") }}</strong></div>
+{% } %}
+
+<style type="text/css" media="screen">
+.modal-dialog {
+	width: 750px;
+}
+
+.table > tbody > tr > td, .table > tfoot > tr > td {
+	padding: 4px;
+}
+</style>
\ No newline at end of file
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
new file mode 100644
index 0000000..6a06930
--- /dev/null
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -0,0 +1,333 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+import json
+from datetime import date, timedelta
+
+import frappe
+import unittest
+from erpnext.stock.doctype.shipment.shipment import fetch_shipping_rates
+from erpnext.stock.doctype.shipment.shipment import create_shipment
+from erpnext.stock.doctype.shipment.shipment import update_tracking
+
+class TestShipment(unittest.TestCase):
+	pass
+
+	def test_shipment_booking(self):
+		shipment = create_test_shipment()
+		try:
+			shipment.submit()
+		except:
+			frappe.throw('Error occurred on submit shipment')
+		doc, rate, tracking_data = make_shipment_transaction(shipment)
+		if doc and rate and tracking_data:
+			self.assertEqual(doc.service_provider, rate.get('service_provider'))
+			self.assertEqual(doc.shipment_amount, rate.get('actual_price'))
+			self.assertEqual(doc.carrier, rate.get('carrier'))
+			self.assertEqual(doc.tracking_status, tracking_data.get('tracking_status'))
+			self.assertEqual(doc.tracking_url, tracking_data.get('tracking_url'))
+
+	def test_shipment_from_delivery_note(self):
+		delivery_note = create_test_delivery_note()
+		try:
+			delivery_note.submit()
+		except:
+			frappe.throw('An error occurred.')
+		
+		shipment = create_test_shipment([ delivery_note ])
+		try:
+			shipment.submit()
+		except:
+			frappe.throw('Error occurred on submit shipment')
+		doc, rate, tracking_data = make_shipment_transaction(shipment)
+		if doc and rate and tracking_data:
+			self.assertEqual(doc.service_provider, rate.get('service_provider'))
+			self.assertEqual(doc.shipment_amount, rate.get('actual_price'))
+			self.assertEqual(doc.carrier, rate.get('carrier'))
+			self.assertEqual(doc.tracking_status, tracking_data.get('tracking_status'))
+			self.assertEqual(doc.tracking_url, tracking_data.get('tracking_url'))
+
+		
+
+def make_shipment_transaction(shipment):
+	shipment_parcel = convert_shipmet_parcel(shipment.shipment_parcel)
+	shipment_rates = fetch_shipping_rates(shipment.pickup_from_type, shipment.delivery_to_type, 
+		shipment.pickup_address_name, shipment.delivery_address_name,
+		shipment_parcel, shipment.description_of_content,
+		shipment.pickup_date, shipment.value_of_goods,
+		pickup_contact_name=shipment.pickup_contact_name,
+		delivery_contact_name=shipment.delivery_contact_name
+	)
+	if len(shipment_rates) > 0:
+		# We are taking the first shipment rate
+		rate = shipment_rates[0]
+		new_shipment = create_shipment(
+			shipment=shipment.name,
+			pickup_from_type=shipment.pickup_from_type,
+			delivery_to_type=shipment.delivery_to_type,
+			pickup_address_name=shipment.pickup_address_name,
+			delivery_address_name=shipment.delivery_address_name,
+			shipment_parcel=shipment_parcel,
+			description_of_content=shipment.description_of_content,
+			pickup_date=shipment.pickup_date,
+			pickup_contact_name=shipment.pickup_contact_name,
+			delivery_contact_name=shipment.delivery_contact_name,
+			value_of_goods=shipment.value_of_goods,
+			service_data=json.dumps(rate),
+			shipment_notific_email=None,
+			tracking_notific_email=None,
+			delivery_notes=None
+		)
+		service_provider = rate.get('service_provider')
+		shipment_id = new_shipment.get('shipment_id')
+		tracking_data = update_tracking(
+			shipment.name,
+			service_provider,
+			shipment_id,
+			delivery_notes=None
+		)
+		doc = frappe.get_doc('Shipment', shipment.name)
+		return doc, rate, tracking_data
+	return None, None, None
+
+def create_test_delivery_note():
+	company = get_shipment_company()
+	customer = get_shipment_customer()
+	item = get_shipment_item(company.name)
+	posting_date = date.today() + timedelta(days=1)
+	
+	create_material_receipt(item, company.name)
+	delivery_note = frappe.new_doc("Delivery Note")
+	delivery_note.company = company.name
+	delivery_note.posting_date = posting_date.strftime("%Y-%m-%d")
+	delivery_note.posting_time = '10:00'
+	delivery_note.customer = customer.name
+	delivery_note.append('items',
+		{
+			"item_code": item.name,
+			"item_name": item.item_name,
+			"description": 'Test delivery note for shipment',
+			"qty": 5,
+			"uom": 'Nos',
+			"warehouse": 'Stores - SC',
+			"rate": item.standard_rate,
+			"cost_center": 'Main - SC'
+		}
+	)
+	delivery_note.insert()
+	frappe.db.commit()
+	return delivery_note
+
+
+def create_test_shipment(delivery_notes=[]):
+	company = get_shipment_company()
+	company_address = get_shipment_company_address(company.name)
+	customer = get_shipment_customer()
+	customer_address = get_shipment_customer_address(customer.name)
+	customer_contact = get_shipment_customer_contact(customer.name)
+	posting_date = date.today() + timedelta(days=5)
+
+	shipment = frappe.new_doc("Shipment")
+	shipment.pickup_from_type = 'Company'
+	shipment.pickup_company = company.name
+	shipment.pickup_address_name = company_address.name
+	shipment.delivery_to_type = 'Customer'
+	shipment.delivery_customer = customer.name
+	shipment.delivery_address_name = customer_address.name
+	shipment.delivery_contact_name = customer_contact.name
+	shipment.pallets = 'No'
+	shipment.shipment_type = 'Goods'
+	shipment.value_of_goods = 1000
+	shipment.pickup_type = 'Pickup'
+	shipment.pickup_date = posting_date.strftime("%Y-%m-%d")
+	shipment.pickup_from = '09:00'
+	shipment.pickup_to = '17:00'
+	shipment.description_of_content = 'unit test entry'
+	for delivery_note in delivery_notes:
+		shipment.append('shipment_delivery_notes', 
+			{
+				"delivery_note": delivery_note.name
+			}
+		)
+	shipment.append('shipment_parcel',
+		{
+			"length": 5,
+			"width": 5,
+			"height": 5,
+			"weight": 5,
+			"count": 5
+		}
+	)
+	shipment.insert()
+	frappe.db.commit()
+	return shipment
+
+
+def get_shipment_customer_contact(customer_name):
+	contact_fname = 'Customer Shipment'
+	contact_lname = 'Testing'
+	customer_name = contact_fname + ' ' + contact_lname
+	contacts = frappe.get_all("Contact", fields=["name"], filters = {"name": customer_name})
+	if len(contacts):
+		return contacts[0]
+	else:
+		return create_customer_contact(contact_fname, contact_lname)
+
+
+def get_shipment_customer_address(customer_name):
+	address_title = customer_name + ' address 123'
+	customer_address = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title})
+	if len(customer_address):
+		return customer_address[0]
+	else:
+		return create_shipment_address(address_title, customer_name, 81929)
+
+def get_shipment_customer():
+	customer_name = 'Shipment Customer'
+	customer = frappe.get_all("Customer", fields=["name"], filters = {"name": customer_name})
+	if len(customer):
+		return customer[0]
+	else:
+		return create_shipment_customer(customer_name)
+
+def get_shipment_company_address(company_name):
+	address_title = company_name + ' address 123'
+	addresses = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title})
+	if len(addresses):
+		return addresses[0]
+	else:
+		return create_shipment_address(address_title, company_name, 80331)
+
+def get_shipment_company():
+	company_name = 'Shipment Company'
+	abbr = 'SC'
+	companies = frappe.get_all("Company", fields=["name"], filters = {"company_name": company_name})
+	if len(companies):
+		return companies[0]
+	else:
+		return create_shipment_company(company_name, abbr)
+
+def get_shipment_item(company_name):
+	item_name = 'Testing Shipment item'
+	items = frappe.get_all("Item",
+		fields=["name", "item_name", "item_code", "standard_rate"],
+		filters = {"item_name": item_name}
+	)
+	if len(items):
+		return items[0]
+	else:
+		return create_shipment_item(item_name, company_name)
+
+def create_shipment_address(address_title, company_name, postal_code):
+	address = frappe.new_doc("Address")
+	address.address_title = address_title
+	address.address_type = 'Shipping'
+	address.address_line1 = company_name + ' address line 1'
+	address.city = 'Random City'
+	address.postal_code = postal_code
+	address.country = 'Germany'
+	address.insert()
+	return address
+
+
+def create_customer_contact(fname, lname):
+	customer = frappe.new_doc("Contact")
+	customer.customer_name = fname + ' ' + lname
+	customer.first_name = fname
+	customer.last_name = lname
+	customer.is_primary_contact = 1
+	customer.is_billing_contact = 1
+	customer.append('email_ids',
+		{
+			'email_id': 'randomme@email.com',
+			'is_primary': 1
+		}
+	)
+	customer.append('phone_nos',
+		{
+			'phone': '123123123',
+			'is_primary_phone': 1,
+			'is_primary_mobile_no': 1
+		}
+	)
+	customer.status = 'Passive'
+	customer.insert()
+	return customer
+
+
+def create_shipment_company(company_name, abbr):
+	company = frappe.new_doc("Company")
+	company.company_name = company_name
+	company.abbr = abbr
+	company.default_currency = 'EUR'
+	company.country = 'Germany'
+	company.insert()
+	return company
+
+def create_shipment_customer(customer_name):
+	customer = frappe.new_doc("Customer")
+	customer.customer_name = customer_name
+	customer.customer_type = 'Company'
+	customer.customer_group = 'All Customer Groups'
+	customer.territory = 'All Territories'
+	customer.gst_category = 'Unregistered'
+	customer.insert()
+	return customer
+
+def create_material_receipt(item, company):
+	posting_date = date.today()
+	stock = frappe.new_doc("Stock Entry")
+	stock.company = company
+	stock.stock_entry_type = 'Material Receipt'
+	stock.posting_date = posting_date.strftime("%Y-%m-%d")
+	stock.append('items',
+		{
+			"t_warehouse": 'Stores - SC',
+			"item_code": item.name,
+			"qty": 5,
+			"uom": 'Nos',
+			"basic_rate": item.standard_rate,
+			"cost_center": 'Main - SC'
+		}
+	)
+	stock.insert()
+	try:
+		stock.submit()
+	except:
+		frappe.throw('An error occurred.')
+	
+
+def create_shipment_item(item_name, company_name):
+	item = frappe.new_doc("Item")
+	item.item_name = item_name
+	item.item_code = item_name
+	item.item_group = 'All Item Groups'
+	item.opening_stock = 'Nos'
+	item.standard_rate = 50
+	item.append('item_defaults',
+		{
+			"company": company_name,
+			"default_warehouse": 'Stores - SC'
+		}
+	)
+	try:
+		item.insert()
+	except:
+		frappe.throw('An error occurred.')
+	return item
+
+
+def convert_shipmet_parcel(shipmet_parcel):
+	data = []
+	for parcel in shipmet_parcel:
+		data.append(
+			{
+				"length": parcel.length,
+				"width": parcel.width,
+				"height": parcel.height,
+				"weight": parcel.weight,
+				"count": parcel.count
+			}
+		)
+	return json.dumps(data)
diff --git a/erpnext/stock/doctype/shipment_delivery_notes/__init__.py b/erpnext/stock/doctype/shipment_delivery_notes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_delivery_notes/__init__.py
diff --git a/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json b/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json
new file mode 100644
index 0000000..fbc01d9
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json
@@ -0,0 +1,41 @@
+{
+ "actions": [],
+ "creation": "2020-07-09 11:52:57.939021",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "delivery_note",
+  "grand_total"
+ ],
+ "fields": [
+  {
+   "fieldname": "delivery_note",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Delivery Note",
+   "options": "Delivery Note",
+   "reqd": 1
+  },
+  {
+   "fetch_from": "delivery_note.grand_total",
+   "fieldname": "grand_total",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Value",
+   "read_only": 1
+  }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-09 12:55:01.134270",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Shipment Delivery Notes",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py b/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py
new file mode 100644
index 0000000..ed936c6
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ShipmentDeliveryNotes(Document):
+	pass
diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/__init__.py b/erpnext/stock/doctype/shipment_notification_subscriptions/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_notification_subscriptions/__init__.py
diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json b/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json
new file mode 100644
index 0000000..bd9b800
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "creation": "2020-07-09 12:49:09.185552",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "email",
+  "unsubscribed"
+ ],
+ "fields": [
+  {
+   "fieldname": "email",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "email",
+   "reqd": 1,
+   "unique": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "unsubscribed",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "unsubscribed"
+  }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-09 12:55:14.217387",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Shipment Notification Subscriptions",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py b/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py
new file mode 100644
index 0000000..28ead7f
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ShipmentNotificationSubscriptions(Document):
+	pass
diff --git a/erpnext/stock/doctype/shipment_parcel/__init__.py b/erpnext/stock/doctype/shipment_parcel/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_parcel/__init__.py
diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json
new file mode 100644
index 0000000..6943edc
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "creation": "2020-07-09 11:28:48.887737",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "length",
+  "width",
+  "height",
+  "weight",
+  "count"
+ ],
+ "fields": [
+  {
+   "fieldname": "length",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Length (cm)",
+   "reqd": 1
+  },
+  {
+   "fieldname": "width",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Width (cm)",
+   "reqd": 1
+  },
+  {
+   "fieldname": "height",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Height (cm)",
+   "reqd": 1
+  },
+  {
+   "fieldname": "weight",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Weight (kg)",
+   "precision": "1",
+   "reqd": 1
+  },
+  {
+   "default": "1",
+   "fieldname": "count",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Count",
+   "reqd": 1
+  }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-09 12:54:14.847170",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Shipment Parcel",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py
new file mode 100644
index 0000000..53e6ed5
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ShipmentParcel(Document):
+	pass
diff --git a/erpnext/stock/doctype/shipment_parcel_template/__init__.py b/erpnext/stock/doctype/shipment_parcel_template/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_parcel_template/__init__.py
diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js
new file mode 100644
index 0000000..785a3b3
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Shipment Parcel Template', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json
new file mode 100644
index 0000000..ec2bb1c
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json
@@ -0,0 +1,78 @@
+{
+ "actions": [],
+ "autoname": "field:preset_name",
+ "creation": "2020-07-09 11:43:43.470339",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "preset_name",
+  "length",
+  "width",
+  "height",
+  "weight"
+ ],
+ "fields": [
+  {
+   "fieldname": "preset_name",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Preset Name",
+   "reqd": 1,
+   "unique": 1
+  },
+  {
+   "fieldname": "length",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Length (cm)",
+   "reqd": 1
+  },
+  {
+   "fieldname": "width",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Width (cm)",
+   "reqd": 1
+  },
+  {
+   "fieldname": "height",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Height (cm)",
+   "reqd": 1
+  },
+  {
+   "fieldname": "weight",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Weight (kg)",
+   "precision": "1",
+   "reqd": 1
+  }
+ ],
+ "links": [],
+ "modified": "2020-07-10 12:53:22.772826",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Shipment Parcel Template",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py
new file mode 100644
index 0000000..2a8d58d
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ShipmentParcelTemplate(Document):
+	pass
diff --git a/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py b/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py
new file mode 100644
index 0000000..6e2caa7
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestShipmentParcelTemplate(unittest.TestCase):
+	pass
diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/__init__.py b/erpnext/stock/doctype/shipment_status_update_subscriptions/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_status_update_subscriptions/__init__.py
diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json b/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json
new file mode 100644
index 0000000..3b86b40
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "creation": "2020-07-09 12:51:10.656612",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "email",
+  "unsubscribed"
+ ],
+ "fields": [
+  {
+   "fieldname": "email",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "email",
+   "reqd": 1,
+   "unique": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "unsubscribed",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "unsubscribed"
+  }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-07-09 12:55:27.615463",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Shipment Status Update Subscriptions",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py b/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py
new file mode 100644
index 0000000..a8e31ea
--- /dev/null
+++ b/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ShipmentStatusUpdateSubscriptions(Document):
+	pass