From 2175b90b8d4bf3fd428b1e1f18702d83aba2b608 Mon Sep 17 00:00:00 2001 From: Valmik Date: Fri, 31 Aug 2018 16:39:29 +0530 Subject: [PATCH] Added feature to optimize routes, also cleaned up code (#15193) --- .../doctype/delivery_stop/delivery_stop.json | 24 ++++- .../doctype/delivery_trip/delivery_trip.js | 22 +++-- .../doctype/delivery_trip/delivery_trip.json | 35 +++++++- .../doctype/delivery_trip/delivery_trip.py | 87 +++++++++---------- 4 files changed, 114 insertions(+), 54 deletions(-) mode change 100644 => 100755 erpnext/stock/doctype/delivery_trip/delivery_trip.js diff --git a/erpnext/stock/doctype/delivery_stop/delivery_stop.json b/erpnext/stock/doctype/delivery_stop/delivery_stop.json index cd71f2128ce..4fe4102180f 100644 --- a/erpnext/stock/doctype/delivery_stop/delivery_stop.json +++ b/erpnext/stock/doctype/delivery_stop/delivery_stop.json @@ -14,6 +14,7 @@ "fields": [ { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -46,6 +47,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -78,6 +80,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -108,6 +111,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -139,6 +143,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -169,6 +174,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -201,6 +207,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -231,6 +238,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -262,6 +270,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -292,12 +301,13 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "fieldname": "estimated_arrival", - "fieldtype": "Time", + "fieldtype": "Datetime", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -323,6 +333,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -354,6 +365,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -384,6 +396,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -416,6 +429,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -446,6 +460,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 1, "bold": 0, "collapsible": 0, @@ -477,6 +492,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -507,6 +523,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -547,7 +564,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-02-22 16:43:55.257470", + "modified": "2018-08-21 22:25:53.276548", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Stop", @@ -561,5 +578,6 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1, - "track_seen": 0 + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js old mode 100644 new mode 100755 index 3e7dfc9bad3..f3c0f752fd9 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -66,17 +66,27 @@ frappe.ui.form.on('Delivery Trip', { calculate_arrival_time: function (frm) { frappe.call({ - method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.calculate_time_matrix', + method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.get_arrival_times', freeze: true, freeze_message: __("Updating estimated arrival times."), args: { - name: frm.doc.name + name: frm.doc.name, + }, + callback: function (r) { + frm.reload_doc(); + } + }); + }, + + optimize_route: function (frm) { + frappe.call({ + method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.optimize_route', + freeze: true, + freeze_message: __("Optimizing routes."), + args: { + name: frm.doc.name, }, callback: function (r) { - if (r.message.error) { - frappe.throw(__("Malformatted address for {0}, please fix to continue.", - [r.message.error.destination.address])); - } frm.reload_doc(); } }); diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json index 0b09e83ad89..36f71a760e1 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json @@ -467,6 +467,39 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "optimize_route", + "fieldtype": "Button", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Optimize Route", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -575,7 +608,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 14:44:36.993178", + "modified": "2018-08-29 14:44:36.993178", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Trip", diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 955783a4c6e..9e55f8ca7db 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -5,15 +5,16 @@ from __future__ import unicode_literals import datetime import frappe -import googlemaps from frappe import _ from frappe.model.document import Document from frappe.utils.user import get_user_fullname -from frappe.utils import getdate, cstr +from frappe.utils import getdate, cstr, get_datetime +from frappe.contacts.doctype.address.address import get_address_display class DeliveryTrip(Document): pass + def get_default_contact(out, name): contact_persons = frappe.db.sql( """ @@ -80,61 +81,59 @@ def get_contact_display(contact): } return contact_info.html -@frappe.whitelist() -def calculate_time_matrix(name): - """Calucation and round in closest 15 minutes, delivery stops""" - gmaps = frappe.db.get_value('Google Maps', None, - ['client_key', 'enabled', 'home_address'], as_dict=1) +def process_route(name, optimize): + doc = frappe.get_doc("Delivery Trip", name) + settings = frappe.get_single("Google Maps Settings") + gmaps_client = settings.get_client() - if not gmaps.enabled: + if not settings.enabled: frappe.throw(_("Google Maps integration is not enabled")) + home_address = get_address_display(frappe.get_doc("Address", settings.home_address).as_dict()) + address_list = [] + + for stop in doc.delivery_stops: + address_list.append(stop.customer_address) + + # Cannot add datetime.date to datetime.timedelta + departure_datetime = get_datetime(doc.date) + doc.departure_time + try: - gmaps_client = googlemaps.Client(key=gmaps.client_key) + directions = gmaps_client.directions(origin=home_address, + destination=home_address, waypoints=address_list, + optimize_waypoints=optimize, departure_time=departure_datetime) except Exception as e: - frappe.throw(e.message) + frappe.throw((e.message)) - secs_15min = 900 - doc = frappe.get_doc('Delivery Trip', name) - departure_time = doc.departure_time - matrix_duration = [] + if not directions: + return - for i, stop in enumerate(doc.delivery_stops): - if i == 0: - # The first row is the starting pointing - origin = gmaps.home_address - destination = format_address(doc.delivery_stops[i].address) - distance_calc = gmaps_client.distance_matrix(origin, destination) - matrix_duration.append(distance_calc) + directions = directions[0] + duration = 0 - try: - distance_secs = distance_calc['rows'][0]['elements'][0]['duration']['value'] - except Exception as e: - frappe.throw(_("Error '{0}' occured. Arguments {1}.").format(e.message, e.args)) + # Google Maps returns the optimized order of the waypoints that were sent + for idx, order in enumerate(directions.get("waypoint_order")): + # We accordingly rearrange the rows + doc.delivery_stops[order].idx = idx + 1 + # Google Maps returns the "legs" in the optimized order, so we loop through it + duration += directions.get("legs")[idx].get("duration").get("value") + arrival_datetime = departure_datetime + datetime.timedelta(seconds=duration) + doc.delivery_stops[order].estimated_arrival = arrival_datetime - stop.estimated_arrival = round_timedelta( - departure_time + datetime.timedelta(0, distance_secs + secs_15min), - datetime.timedelta(minutes=15)) - else: - # Calculation based on previous - origin = format_address(doc.delivery_stops[i - 1].address) - destination = format_address(doc.delivery_stops[i].address) - distance_calc = gmaps_client.distance_matrix(origin, destination) - matrix_duration.append(distance_calc) + doc.save() + frappe.db.commit() - try: - distance_secs = distance_calc['rows'][0]['elements'][0]['duration']['value'] - except Exception as e: - frappe.throw(_("Error '{0}' occured. Arguments {1}.").format(e.message, e.args)) - stop.estimated_arrival = round_timedelta( - doc.delivery_stops[i - 1].estimated_arrival + - datetime.timedelta(0, distance_secs + secs_15min), datetime.timedelta(minutes=15)) - stop.save() - frappe.db.commit() +@frappe.whitelist() +def optimize_route(name): + process_route(name, optimize=True) + + +@frappe.whitelist() +def get_arrival_times(name): + process_route(name, optimize=False) - return matrix_duration @frappe.whitelist() def notify_customers(docname, date, driver, vehicle, sender_email, delivery_notification):