diff --git a/erpnext/erpnext_integrations/doctype/letmeship/__init__.py b/erpnext/erpnext_integrations/doctype/letmeship/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js new file mode 100644 index 00000000000..1e5e372dff7 --- /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 00000000000..4a9a70f2510 --- /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 00000000000..3ad06dbb58f --- /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 00000000000..3439e4fd728 --- /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 00000000000..e69de29bb2d diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.js b/erpnext/erpnext_integrations/doctype/packlink/packlink.js new file mode 100644 index 00000000000..da864584f6d --- /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 00000000000..a56595e9a1a --- /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 00000000000..7fdb053cf84 --- /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 00000000000..106ae51f7cc --- /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 00000000000..e69de29bb2d diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js new file mode 100644 index 00000000000..3b852368635 --- /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 00000000000..dab54cba6c9 --- /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 00000000000..85c94388dc5 --- /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 = \ + '{{ _("Click here to Track Shipment") }}
' + 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 00000000000..5cbe80e8ac1 --- /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 e278fd78071..e7ef4c8ebd5 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -60,4 +60,14 @@ def create_mode_of_payment(gateway, payment_type="General"): "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 = '{{ _("Click here to Track Shipment") }}' + 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 251a26a592e..03921c554e3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -156,6 +156,11 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } 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 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } }, + 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 d04cf785ab1..00a66fa48e6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -569,6 +569,59 @@ def make_packing_slip(source_name, target_doc=None): 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 += '
' + user.email + if user.phone: + pickup_contact_display += '
' + user.phone + if user.mobile_no and not user.phone: + pickup_contact_display += '
' + 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 += '
' + contact.email_id + if contact.phone: + delivery_contact_display += '
' + contact.phone + if contact.mobile_no and not contact.phone: + delivery_contact_display += '
' + 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 00000000000..e69de29bb2d 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 00000000000..43b8ed5bf85 --- /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 00000000000..9960acf4aeb --- /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 00000000000..e46ac76ef71 --- /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 00000000000..c2f96d9cb0e --- /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 00000000000..e69de29bb2d 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 00000000000..31d54536c08 --- /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 00000000000..3c0c4d5f807 --- /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 00000000000..b55528c3594 --- /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 00000000000..e214264accd --- /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 00000000000..e69de29bb2d 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 00000000000..8e7731e6c17 --- /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 00000000000..fd0a7d8b498 --- /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 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/shipment/api/utils.py b/erpnext/stock/doctype/shipment/api/utils.py new file mode 100644 index 00000000000..1153933e81a --- /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.
\ + Please set Postal Code for Address {1}" + ).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.
\ + Please set Last Name for Contact {1}" + ).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 00000000000..e9f4484ab11 --- /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.
+ Please set Email/Phone for the contact ${contact_name}`)); + } + let contact_display = r.message.contact_display; + if (r.message.contact_email) { + contact_display += '
' + r.message.contact_email; + } + if (r.message.contact_phone) { + contact_display += '
' + r.message.contact_phone; + } + if (r.message.contact_mobile && !r.message.contact_phone) { + contact_display += '
' + 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.
+ Please first set Last Name, Email and Phone for the user ${frappe.session.user}`)); + } + let contact_display = r.full_name; + if (r.email) { + contact_display += '
' + r.email; + } + if (r.phone) { + contact_display += '
' + r.phone; + } + if (r.mobile_no && !r.phone) { + contact_display += '
' + 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 00000000000..b6656a2b72c --- /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 00000000000..e059bacfa13 --- /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.
\ + Please set Postal Code for Address {1}" + ).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.
\ + Please set Last Name for Contact {1}" + ).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 00000000000..57e92099cb2 --- /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 00000000000..ed9b8bf4003 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment_service_selector.html @@ -0,0 +1,70 @@ +{% if (data.length) { %} +
+
{{ __("Preferred Services") }}
+ + + + {% for (var i = 0; i < header_columns.length; i++) { %} + + {% } %} + + + + {% for (var i = 0; i < data.length; i++) { %} + {% if (data[i].is_preferred) { %} + + + + + + + + {% } %} + {% } %} + +
{{ header_columns[i] }}
{{ data[i].service_provider }}{{ data[i].carrier }}{{ data[i].service_name }}{{ format_currency(data[i].total_price, 'EUR', 2) }} + +
+
{{ __("Other Services") }}
+ + + + {% for (var i = 0; i < header_columns.length; i++) { %} + + {% } %} + + + + {% for (var i = 0; i < data.length; i++) { %} + {% if (!data[i].is_preferred) { %} + + + + + + + + {% } %} + {% } %} + +
{{ header_columns[i] }}
{{ data[i].service_provider }}{{ data[i].carrier }}{{ data[i].service_name }}{{ format_currency(data[i].total_price, 'EUR', 2) }} + +
+
+{% } else { %} +
{{ __("No Services Available") }}
+{% } %} + + \ 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 00000000000..6a06930e82f --- /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 00000000000..e69de29bb2d 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 00000000000..fbc01d9a247 --- /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 00000000000..ed936c60f8b --- /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 00000000000..e69de29bb2d 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 00000000000..bd9b8003a88 --- /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 00000000000..28ead7fab83 --- /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 00000000000..e69de29bb2d 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 00000000000..6943edcdc91 --- /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 00000000000..53e6ed55dd3 --- /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 00000000000..e69de29bb2d 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 00000000000..785a3b304de --- /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 00000000000..ec2bb1c9b32 --- /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 00000000000..2a8d58d8305 --- /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 00000000000..6e2caa768bf --- /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 00000000000..e69de29bb2d 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 00000000000..3b86b400d87 --- /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 00000000000..a8e31ea778a --- /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