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