diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index a9aa5ae60e..bb94383aed 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '10.1.68'
+__version__ = '10.1.70'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index aec3d1b014..f213ffa658 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -933,7 +933,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
return paid_amount[0][0] if paid_amount else 0
@frappe.whitelist()
-def get_party_and_account_balance(company, date, paid_from, paid_to=None, ptype=None, pty=None, cost_center=None):
+def get_party_and_account_balance(company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None):
return frappe._dict({
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index 42f371bddd..fe99763a35 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -201,8 +201,8 @@ def get_pricing_rule_for_item(args):
"discount_percentage": 0.0
})
else:
- item_details.discount_percentage = pricing_rule.discount_percentage or args.discount_percentage
-
+ item_details.discount_percentage = (pricing_rule.get('discount_percentage', 0)
+ if pricing_rule else args.discount_percentage)
elif args.get('pricing_rule'):
item_details = remove_pricing_rule_for_item(args.get("pricing_rule"), item_details)
@@ -393,4 +393,4 @@ def make_pricing_rule(doctype, docname):
doc.selling = 1 if doctype == "Customer" else 0
doc.buying = 1 if doctype == "Supplier" else 0
- return doc
\ No newline at end of file
+ return doc
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 1b63f7105f..6387003f01 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -662,9 +662,6 @@ class SalesInvoice(SellingController):
def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False):
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
- if not self.grand_total:
- return
-
if not gl_entries:
gl_entries = self.get_gl_entries()
diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
index 2508a1f4c2..bd2c34b3b4 100644
--- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
+++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
@@ -21,6 +21,8 @@ def get_data(filters, show_party_name):
party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type')))
if filters.get('party_type') == 'Student':
party_name_field = 'first_name'
+ elif filters.get('party_type') == 'Shareholder':
+ party_name_field = 'title'
party_filters = {"name": filters.get("party")} if filters.get("party") else {}
parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field],
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index e277602f0a..51747f67ae 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -117,6 +117,13 @@ class AccountsController(TransactionBase):
if self.get("group_same_items"):
self.group_similar_items()
+ df = self.meta.get_field("discount_amount")
+ if self.get("discount_amount") and hasattr(self, "taxes") and not len(self.taxes):
+ df.set("print_hide", 0)
+ self.discount_amount = -self.discount_amount
+ else:
+ df.set("print_hide", 1)
+
def validate_paid_amount(self):
if hasattr(self, "is_pos") or hasattr(self, "is_paid"):
is_paid = self.get("is_pos") or self.get("is_paid")
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 57c6556a03..719f4c732a 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -345,7 +345,8 @@ class SellingController(StockController):
sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
if sales_orders:
po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)})
- self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no])))
+ if po_nos and po_nos[0].get('po_no'):
+ self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no])))
def validate_items(self):
# validate items to see if they have is_sales_item enabled
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 2005147e7b..87b7942070 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -11,8 +11,8 @@ app_email = "info@erpnext.com"
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
-develop_version = '11.x.x-develop'
-staging_version = '11.0.3-beta.19'
+develop_version = '12.x.x-develop'
+staging_version = '11.0.3-beta.20'
error_report_email = "support@erpnext.com"
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index a55c3c5778..8c5f2af96a 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -54,7 +54,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "depends_on": "eval:!doc.__islocal",
+ "depends_on": "",
"fieldname": "item_name",
"fieldtype": "Data",
"hidden": 0,
@@ -1976,7 +1976,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2018-10-11 11:52:39.047935",
+ "modified": "2018-10-24 02:07:21.618275",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.js b/erpnext/setup/doctype/global_defaults/global_defaults.js
index a78461d9bc..58b8c51228 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.js
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.js
@@ -1,10 +1,35 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
$.extend(cur_frm.cscript, {
- validate: function(doc, cdt, cdn) {
- return $c_obj(doc, 'get_defaults', '', function(r, rt){
+ onload: function (doc, cdt, cdn) {
+ cur_frm.trigger("get_distance_uoms");
+ },
+
+ validate: function (doc, cdt, cdn) {
+ return $c_obj(doc, 'get_defaults', '', function (r, rt) {
frappe.sys_defaults = r.message;
});
+ },
+
+ get_distance_uoms: function (frm) {
+ let units = [];
+
+ frappe.call({
+ method: "frappe.client.get_list",
+ args: {
+ doctype: "UOM Conversion Factor",
+ filters: { "category": "Length" },
+ fields: ["to_uom"],
+ limit_page_length: 500
+ },
+ callback: function (r) {
+ r.message.forEach(row => units.push(row.to_uom));
+ }
+ });
+
+ cur_frm.set_query("default_distance_unit", function (doc) {
+ return { filters: { "name": ["IN", units] } };
+ })
}
});
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json
index a6c59649e8..bafb97a5d8 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.json
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.json
@@ -1,122 +1,183 @@
{
"allow_copy": 1,
- "allow_email": 0,
+ "allow_guest_to_view": 0,
"allow_import": 0,
- "allow_print": 0,
"allow_rename": 0,
- "allow_trash": 0,
+ "beta": 0,
"creation": "2013-05-02 17:53:24",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
+ "editable_grid": 0,
"fields": [
{
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
+ "columns": 0,
"fieldname": "default_company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
+ "in_global_search": 0,
"in_list_view": 0,
+ "in_standard_filter": 0,
"label": "Default Company",
"length": 0,
- "no_column": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
+ "columns": 0,
"fieldname": "current_fiscal_year",
"fieldtype": "Link",
"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": "Current Fiscal Year",
"length": 0,
- "no_column": 0,
"no_copy": 0,
"options": "Fiscal Year",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
+ "remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
+ "columns": 0,
"fieldname": "country",
"fieldtype": "Link",
"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": "Country",
"length": 0,
- "no_column": 0,
"no_copy": 0,
"options": "Country",
"permlevel": 0,
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
+ "columns": 0,
+ "default": "",
+ "fieldname": "default_distance_unit",
+ "fieldtype": "Link",
+ "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": "Default Distance Unit",
+ "length": 0,
+ "no_copy": 0,
+ "options": "UOM",
+ "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,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
"fieldname": "column_break_8",
"fieldtype": "Column Break",
"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,
"length": 0,
- "no_column": 0,
"no_copy": 0,
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
+ "columns": 0,
"default": "INR",
"fieldname": "default_currency",
"fieldtype": "Link",
@@ -124,26 +185,32 @@
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
+ "in_global_search": 0,
"in_list_view": 1,
+ "in_standard_filter": 0,
"label": "Default Currency",
"length": 0,
- "no_column": 0,
"no_copy": 0,
"options": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
+ "remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
+ "translatable": 0,
"unique": 0
},
{
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
+ "columns": 0,
"description": "Do not show any symbol like $ etc next to currencies.",
"fieldname": "hide_currency_symbol",
"fieldtype": "Select",
@@ -151,26 +218,32 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
+ "in_global_search": 0,
"in_list_view": 1,
+ "in_standard_filter": 0,
"label": "Hide Currency Symbol",
"length": 0,
- "no_column": 0,
"no_copy": 0,
"options": "\nNo\nYes",
"permlevel": 0,
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
+ "columns": 0,
"description": "If disable, 'Rounded Total' field will not be visible in any transaction",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
@@ -178,25 +251,31 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
+ "in_global_search": 0,
"in_list_view": 1,
+ "in_standard_filter": 0,
"label": "Disable Rounded Total",
"length": 0,
- "no_column": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
+ "columns": 0,
"description": "If disable, 'In Words' field will not be visible in any transaction",
"fieldname": "disable_in_words",
"fieldtype": "Check",
@@ -204,7 +283,9 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
+ "in_global_search": 0,
"in_list_view": 1,
+ "in_standard_filter": 0,
"label": "Disable In Words",
"length": 0,
"no_copy": 0,
@@ -213,26 +294,28 @@
"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
}
],
+ "has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-cog",
"idx": 1,
+ "image_view": 0,
"in_create": 1,
-
"is_submittable": 0,
- "is_transaction_doc": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
- "modified": "2016-03-03 16:14:41.260467",
+ "modified": "2018-10-15 03:08:19.886212",
"modified_by": "Administrator",
"module": "Setup",
"name": "Global Defaults",
@@ -240,7 +323,6 @@
"permissions": [
{
"amend": 0,
- "apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
@@ -252,7 +334,6 @@
"print": 0,
"read": 1,
"report": 0,
- "restrict": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
@@ -260,10 +341,12 @@
"write": 1
}
],
+ "quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
- "show_in_menu": 0,
+ "show_name_in_global_search": 0,
"sort_order": "DESC",
- "use_template": 0,
- "version": 0
+ "track_changes": 0,
+ "track_seen": 0,
+ "track_views": 0
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/global_defaults/test_global_defaults.js b/erpnext/setup/doctype/global_defaults/test_global_defaults.js
new file mode 100644
index 0000000000..33634eb0f8
--- /dev/null
+++ b/erpnext/setup/doctype/global_defaults/test_global_defaults.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Global Defaults", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Global Defaults
+ () => frappe.tests.make('Global Defaults', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/setup/doctype/global_defaults/test_global_defaults.py b/erpnext/setup/doctype/global_defaults/test_global_defaults.py
new file mode 100644
index 0000000000..0495af7b41
--- /dev/null
+++ b/erpnext/setup/doctype/global_defaults/test_global_defaults.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestGlobalDefaults(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/delivery_settings/delivery_settings.json b/erpnext/stock/doctype/delivery_settings/delivery_settings.json
index b70d74d1a6..963403b8f2 100644
--- a/erpnext/stock/doctype/delivery_settings/delivery_settings.json
+++ b/erpnext/stock/doctype/delivery_settings/delivery_settings.json
@@ -143,6 +143,70 @@
"set_only_once": 0,
"translatable": 0,
"unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "cb_delivery",
+ "fieldtype": "Column Break",
+ "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,
+ "length": 0,
+ "no_copy": 0,
+ "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,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "description": "In minutes",
+ "fieldname": "stop_delay",
+ "fieldtype": "Int",
+ "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": "Delay between Delivery Stops",
+ "length": 0,
+ "no_copy": 0,
+ "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
}
],
"has_web_view": 0,
@@ -155,7 +219,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
- "modified": "2018-09-05 00:16:23.569855",
+ "modified": "2018-09-09 23:51:34.279941",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Settings",
diff --git a/erpnext/stock/doctype/delivery_stop/delivery_stop.json b/erpnext/stock/doctype/delivery_stop/delivery_stop.json
index 023e34de08..7bce72dfde 100644
--- a/erpnext/stock/doctype/delivery_stop/delivery_stop.json
+++ b/erpnext/stock/doctype/delivery_stop/delivery_stop.json
@@ -493,6 +493,38 @@
"translatable": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "distance",
+ "fieldtype": "Float",
+ "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": "Distance",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "2",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "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,
@@ -525,6 +557,168 @@
"translatable": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "lat",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Latitude",
+ "length": 0,
+ "no_copy": 0,
+ "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,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break",
+ "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,
+ "length": 0,
+ "no_copy": 0,
+ "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,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "",
+ "depends_on": "eval:doc.distance",
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "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": "UOM",
+ "length": 0,
+ "no_copy": 0,
+ "options": "UOM",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "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,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "lng",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Longitude",
+ "length": 0,
+ "no_copy": 0,
+ "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,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "more_information_section",
+ "fieldtype": "Section Break",
+ "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": "More Information",
+ "length": 0,
+ "no_copy": 0,
+ "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,
@@ -568,7 +762,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
- "modified": "2018-09-05 00:51:55.275009",
+ "modified": "2018-10-11 22:32:27.450906",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Stop",
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js
index 9c31e05f47..a38c6d7a0a 100755
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js
@@ -65,31 +65,43 @@ frappe.ui.form.on('Delivery Trip', {
},
calculate_arrival_time: function (frm) {
- frappe.call({
- method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.get_arrival_times',
- freeze: true,
- freeze_message: __("Updating estimated arrival times."),
- args: {
- name: frm.doc.name,
- },
- callback: function (r) {
- frm.reload_doc();
+ frappe.db.get_value("Google Maps Settings", { name: "Google Maps Settings" }, "enabled", (r) => {
+ if (r.enabled == 0) {
+ frappe.throw(__("Please enable Google Maps Settings to estimate and optimize routes"));
+ } else {
+ frappe.call({
+ method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.get_arrival_times',
+ freeze: true,
+ freeze_message: __("Updating estimated arrival times."),
+ args: {
+ delivery_trip: 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) {
- frm.reload_doc();
+ frappe.db.get_value("Google Maps Settings", {name: "Google Maps Settings"}, "enabled", (r) => {
+ if (r.enabled == 0) {
+ frappe.throw(__("Please enable Google Maps Settings to estimate and optimize routes"));
+ } else {
+ frappe.call({
+ method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.optimize_route',
+ freeze: true,
+ freeze_message: __("Optimizing routes."),
+ args: {
+ delivery_trip: frm.doc.name,
+ },
+ callback: function (r) {
+ frm.reload_doc();
+ }
+ });
}
- });
+ })
},
notify_customers: function (frm) {
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json
index 3cb19af0f2..a9236e88b5 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json
@@ -159,101 +159,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "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,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "departure_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Departure Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "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": "Delivery Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@@ -336,6 +242,104 @@
"translatable": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "total_distance",
+ "fieldtype": "Float",
+ "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": "Total Estimated Distance",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "2",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "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,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "",
+ "depends_on": "eval:doc.total_distance",
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "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": "Distance UOM",
+ "length": 0,
+ "no_copy": 0,
+ "options": "UOM",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "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,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break",
+ "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,
+ "length": 0,
+ "no_copy": 0,
+ "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,
@@ -369,6 +373,38 @@
"translatable": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "departure_time",
+ "fieldtype": "Datetime",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Departure Time",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
@@ -441,7 +477,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "depends_on": "eval:!cur_frm.is_new()",
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "calculate_arrival_time",
"fieldtype": "Button",
"hidden": 0,
@@ -474,6 +510,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "optimize_route",
"fieldtype": "Button",
"hidden": 0,
@@ -574,7 +611,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2018-09-05 01:20:34.165834",
+ "modified": "2018-10-11 22:32:04.355068",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Trip",
@@ -627,5 +664,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
- "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.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
index 427bc2496d..431eb6c6a5 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
@@ -10,17 +10,43 @@ import frappe
from frappe import _
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.document import Document
-from frappe.utils import get_datetime, get_link_to_form, cstr
+from frappe.utils import cint, get_datetime, get_link_to_form
class DeliveryTrip(Document):
+ def __init__(self, *args, **kwargs):
+ super(DeliveryTrip, self).__init__(*args, **kwargs)
+
+ # Google Maps returns distances in meters by default
+ self.default_distance_uom = frappe.db.get_single_value("Global Defaults", "default_distance_unit") or "Meter"
+ self.uom_conversion_factor = frappe.db.get_value("UOM Conversion Factor",
+ {"from_uom": "Meter", "to_uom": self.default_distance_uom},
+ "value")
+
+ def validate(self):
+ self.validate_stop_addresses()
+
def on_submit(self):
self.update_delivery_notes()
def on_cancel(self):
self.update_delivery_notes(delete=True)
+ def validate_stop_addresses(self):
+ for stop in self.delivery_stops:
+ if not stop.customer_address:
+ stop.customer_address = get_address_display(frappe.get_doc("Address", stop.address).as_dict())
+
def update_delivery_notes(self, delete=False):
+ """
+ Update all connected Delivery Notes with Delivery Trip details
+ (Driver, Vehicle, etc.). If `delete` is `True`, then details
+ are removed.
+
+ Args:
+ delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`.
+ """
+
delivery_notes = list(set([stop.delivery_note for stop in self.delivery_stops if stop.delivery_note]))
update_fields = {
@@ -28,7 +54,7 @@ class DeliveryTrip(Document):
"driver_name": self.driver_name,
"vehicle_no": self.vehicle,
"lr_no": self.name,
- "lr_date": self.date
+ "lr_date": self.departure_time
}
for delivery_note in delivery_notes:
@@ -44,58 +70,175 @@ class DeliveryTrip(Document):
delivery_notes = [get_link_to_form("Delivery Note", note) for note in delivery_notes]
frappe.msgprint(_("Delivery Notes {0} updated".format(", ".join(delivery_notes))))
+ def process_route(self, optimize):
+ """
+ Estimate the arrival times for each stop in the Delivery Trip.
+ If `optimize` is True, the stops will be re-arranged, based
+ on the optimized order, before estimating the arrival times.
+
+ Args:
+ optimize (bool): True if route needs to be optimized, else False
+ """
+
+ if not frappe.db.get_single_value("Google Maps Settings", "enabled"):
+ frappe.throw(_("Cannot process route, since Google Maps Settings is disabled."))
+
+ departure_datetime = get_datetime(self.departure_time)
+ route_list = self.form_route_list(optimize)
+
+ # For locks, maintain idx count while looping through route list
+ idx = 0
+ for route in route_list:
+ directions = get_directions(route, optimize)
+
+ if directions:
+ if optimize and len(directions.get("waypoint_order")) > 1:
+ self.rearrange_stops(directions.get("waypoint_order"), start=idx)
+
+ # Avoid estimating last leg back to the home address
+ legs = directions.get("legs")[:-1] if route == route_list[-1] else directions.get("legs")
+
+ # Google Maps returns the legs in the optimized order
+ for leg in legs:
+ delivery_stop = self.delivery_stops[idx]
+
+ delivery_stop.lat, delivery_stop.lng = leg.get("end_location", {}).values()
+ delivery_stop.uom = self.default_distance_uom
+ distance = leg.get("distance", {}).get("value", 0.0) # in meters
+ delivery_stop.distance = distance * self.uom_conversion_factor
+
+ duration = leg.get("duration", {}).get("value", 0)
+ estimated_arrival = departure_datetime + datetime.timedelta(seconds=duration)
+ delivery_stop.estimated_arrival = estimated_arrival
+
+ stop_delay = frappe.db.get_single_value("Delivery Settings", "stop_delay")
+ departure_datetime = estimated_arrival + datetime.timedelta(minutes=cint(stop_delay))
+ idx += 1
+
+ # Include last leg in the final distance calculation
+ self.uom = self.default_distance_uom
+ total_distance = sum([leg.get("distance", {}).get("value", 0.0)
+ for leg in directions.get("legs")]) # in meters
+ self.total_distance = total_distance * self.uom_conversion_factor
+ else:
+ idx += len(route) - 1
+
+ self.save()
+
+ def form_route_list(self, optimize):
+ """
+ Form a list of address routes based on the delivery stops. If locks
+ are present, and the routes need to be optimized, then they will be
+ split into sublists at the specified lock position(s).
+
+ Args:
+ optimize (bool): `True` if route needs to be optimized, else `False`
+
+ Returns:
+ (list of list of str): List of address routes split at locks, if optimize is `True`
+ """
+
+ settings = frappe.get_single("Google Maps Settings")
+ home_address = get_address_display(frappe.get_doc("Address", settings.home_address).as_dict())
+
+ route_list = []
+ # Initialize first leg with origin as the home address
+ leg = [home_address]
+
+ for stop in self.delivery_stops:
+ leg.append(stop.customer_address)
+
+ if optimize and stop.lock:
+ route_list.append(leg)
+ leg = [stop.customer_address]
+
+ # For last leg, append home address as the destination
+ # only if lock isn't on the final stop
+ if len(leg) > 1:
+ leg.append(home_address)
+ route_list.append(leg)
+
+ route_list = [[sanitize_address(address) for address in route] for route in route_list]
+
+ return route_list
+
+ def rearrange_stops(self, optimized_order, start):
+ """
+ Re-arrange delivery stops based on order optimized
+ for vehicle routing problems.
+
+ Args:
+ optimized_order (list of int): The index-based optimized order of the route
+ start (int): The index at which to start the rearrangement
+ """
+
+ stops_order = []
+
+ # Child table idx starts at 1
+ for new_idx, old_idx in enumerate(optimized_order, 1):
+ new_idx = start + new_idx
+ old_idx = start + old_idx
+
+ self.delivery_stops[old_idx].idx = new_idx
+ stops_order.append(self.delivery_stops[old_idx])
+
+ self.delivery_stops[start:start + len(stops_order)] = stops_order
+
+
+@frappe.whitelist()
+def get_contact_and_address(name):
+ out = frappe._dict()
+
+ get_default_contact(out, name)
+ get_default_address(out, name)
+
+ return out
def get_default_contact(out, name):
contact_persons = frappe.db.sql(
"""
- select parent,
- (select is_primary_contact from tabContact c where c.name = dl.parent)
- as is_primary_contact
- from
+ SELECT parent,
+ (SELECT is_primary_contact FROM tabContact c WHERE c.name = dl.parent) AS is_primary_contact
+ FROM
`tabDynamic Link` dl
- where
- dl.link_doctype="Customer" and
- dl.link_name=%s and
- dl.parenttype = 'Contact'
+ WHERE
+ dl.link_doctype="Customer"
+ AND dl.link_name=%s
+ AND dl.parenttype = "Contact"
""", (name), as_dict=1)
if contact_persons:
for out.contact_person in contact_persons:
if out.contact_person.is_primary_contact:
return out.contact_person
+
out.contact_person = contact_persons[0]
+
return out.contact_person
- else:
- return None
+
def get_default_address(out, name):
shipping_addresses = frappe.db.sql(
"""
- select parent,
- (select is_shipping_address from tabAddress a where a.name=dl.parent) as is_shipping_address
- from `tabDynamic Link` dl
- where link_doctype="Customer"
- and link_name=%s
- and parenttype = 'Address'
+ SELECT parent,
+ (SELECT is_shipping_address FROM tabAddress a WHERE a.name=dl.parent) AS is_shipping_address
+ FROM
+ `tabDynamic Link` dl
+ WHERE
+ dl.link_doctype="Customer"
+ AND dl.link_name=%s
+ AND dl.parenttype = "Address"
""", (name), as_dict=1)
if shipping_addresses:
for out.shipping_address in shipping_addresses:
if out.shipping_address.is_shipping_address:
return out.shipping_address
+
out.shipping_address = shipping_addresses[0]
+
return out.shipping_address
- else:
- return None
-
-
-@frappe.whitelist()
-def get_contact_and_address(name):
- out = frappe._dict()
- get_default_contact(out, name)
- get_default_address(out, name)
- return out
@frappe.whitelist()
@@ -103,67 +246,83 @@ def get_contact_display(contact):
contact_info = frappe.db.get_value(
"Contact", contact,
["first_name", "last_name", "phone", "mobile_no"],
- as_dict=1)
+ as_dict=1)
+
contact_info.html = """ %(first_name)s %(last_name)s
%(phone)s
%(mobile_no)s""" % {
"first_name": contact_info.first_name,
"last_name": contact_info.last_name or "",
"phone": contact_info.phone or "",
- "mobile_no": contact_info.mobile_no or "",
+ "mobile_no": contact_info.mobile_no or ""
}
+
return contact_info.html
-def process_route(name, optimize):
- doc = frappe.get_doc("Delivery Trip", name)
- settings = frappe.get_single("Google Maps Settings")
- gmaps_client = settings.get_client()
+@frappe.whitelist()
+def optimize_route(delivery_trip):
+ delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
+ delivery_trip.process_route(optimize=True)
- 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 = []
+@frappe.whitelist()
+def get_arrival_times(delivery_trip):
+ delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
+ delivery_trip.process_route(optimize=False)
- 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
+def sanitize_address(address):
+ """
+ Remove HTML breaks in a given address
- try:
- 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))
+ Args:
+ address (str): Address to be sanitized
- if not directions:
+ Returns:
+ (str): Sanitized address
+ """
+
+ if not address:
return
- directions = directions[0]
- duration = 0
+ address = address.split('
')
- # 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
-
- doc.save()
- frappe.db.commit()
+ # Only get the first 3 blocks of the address
+ return ', '.join(address[:3])
-@frappe.whitelist()
-def optimize_route(name):
- process_route(name, optimize=True)
+def get_directions(route, optimize):
+ """
+ Retrieve map directions for a given route and departure time.
+ If optimize is `True`, Google Maps will return an optimized
+ order for the intermediate waypoints.
+ NOTE: Google's API does take an additional `departure_time` key,
+ but it only works for routes without any waypoints.
-@frappe.whitelist()
-def get_arrival_times(name):
- process_route(name, optimize=False)
+ Args:
+ route (list of str): Route addresses (origin -> waypoint(s), if any -> destination)
+ optimize (bool): `True` if route needs to be optimized, else `False`
+
+ Returns:
+ (dict): Route legs and, if `optimize` is `True`, optimized waypoint order
+ """
+
+ settings = frappe.get_single("Google Maps Settings")
+ maps_client = settings.get_client()
+
+ directions_data = {
+ "origin": route[0],
+ "destination": route[-1],
+ "waypoints": route[1: -1],
+ "optimize_waypoints": optimize
+ }
+
+ try:
+ directions = maps_client.directions(**directions_data)
+ except Exception as e:
+ frappe.throw(_(e.message))
+
+ return directions[0] if directions else False
@frappe.whitelist()
@@ -171,10 +330,6 @@ def notify_customers(delivery_trip):
delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
context = delivery_trip.as_dict()
- context.update({
- "departure_time": cstr(context.get("departure_time")),
- "estimated_arrival": cstr(context.get("estimated_arrival"))
- })
if delivery_trip.driver:
context.update(frappe.db.get_value("Driver", delivery_trip.driver, "cell_number", as_dict=1))
diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
index fae03f8d9f..b0a3d315ae 100644
--- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
@@ -9,7 +9,7 @@ import erpnext
import frappe
from erpnext.stock.doctype.delivery_trip.delivery_trip import get_contact_and_address, notify_customers
from erpnext.tests.utils import create_test_contact_and_address
-from frappe.utils import add_days, now_datetime, nowdate
+from frappe.utils import add_days, now_datetime
class TestDeliveryTrip(unittest.TestCase):
@@ -19,28 +19,58 @@ class TestDeliveryTrip(unittest.TestCase):
create_delivery_notification()
create_test_contact_and_address()
- def test_delivery_trip(self):
- contact = get_contact_and_address("_Test Customer")
+ settings = frappe.get_single("Google Maps Settings")
+ settings.home_address = frappe.get_last_doc("Address").name
+ settings.save()
- if not frappe.db.exists("Delivery Trip", "TOUR-00000"):
- delivery_trip = frappe.get_doc({
- "doctype": "Delivery Trip",
- "company": erpnext.get_default_company(),
- "date": add_days(nowdate(), 5),
- "departure_time": add_days(now_datetime(), 5),
- "driver": frappe.db.get_value('Driver', {"full_name": "Newton Scmander"}),
- "vehicle": "JB 007",
- "delivery_stops": [{
- "customer": "_Test Customer",
- "address": contact.shipping_address.parent,
- "contact": contact.contact_person.parent
- }]
- })
- delivery_trip.insert()
+ self.delivery_trip = create_delivery_trip()
- notify_customers(delivery_trip=delivery_trip.name)
- delivery_trip.load_from_db()
- self.assertEqual(delivery_trip.email_notification_sent, 1)
+ def tearDown(self):
+ frappe.db.sql("delete from `tabDriver`")
+ frappe.db.sql("delete from `tabVehicle`")
+ frappe.db.sql("delete from `tabEmail Template`")
+ frappe.db.sql("delete from `tabDelivery Trip`")
+
+ def test_delivery_trip_notify_customers(self):
+ notify_customers(delivery_trip=self.delivery_trip.name)
+ self.delivery_trip.load_from_db()
+ self.assertEqual(self.delivery_trip.email_notification_sent, 1)
+
+ def test_unoptimized_route_list_without_locks(self):
+ route_list = self.delivery_trip.form_route_list(optimize=False)
+
+ # Return a single list of destinations, from home address and back
+ self.assertEqual(len(route_list), 1)
+ self.assertEqual(len(route_list[0]), 4)
+
+ def test_unoptimized_route_list_with_locks(self):
+ self.delivery_trip.delivery_stops[0].lock = 1
+ self.delivery_trip.save()
+ route_list = self.delivery_trip.form_route_list(optimize=False)
+
+ # Return a single list of destinations, from home address and back,
+ # since the stops don't need to optimized and simple time
+ # estimation is enough
+ self.assertEqual(len(route_list), 1)
+ self.assertEqual(len(route_list[0]), 4)
+
+ def test_optimized_route_list_without_locks(self):
+ route_list = self.delivery_trip.form_route_list(optimize=True)
+
+ # Return a single list of destinations, from home address and back,
+ # since the route doesn't have any locks to be optimized against
+ self.assertEqual(len(route_list), 1)
+ self.assertEqual(len(route_list[0]), 4)
+
+ def test_optimized_route_list_with_locks(self):
+ self.delivery_trip.delivery_stops[0].lock = 1
+ self.delivery_trip.save()
+ route_list = self.delivery_trip.form_route_list(optimize=True)
+
+ # Return multiple route lists, taking the home address as start and end
+ self.assertEqual(len(route_list), 2)
+ self.assertEqual(len(route_list[0]), 2) # [home_address, locked_stop]
+ self.assertEqual(len(route_list[1]), 3) # [locked_stop, second_stop, home_address]
def create_driver():
@@ -67,6 +97,7 @@ def create_delivery_notification():
delivery_settings = frappe.get_single("Delivery Settings")
delivery_settings.dispatch_template = 'Delivery Notification'
+ delivery_settings.save()
def create_vehicle():
@@ -84,3 +115,30 @@ def create_vehicle():
"vehicle_value": frappe.utils.flt(500000)
})
vehicle.insert()
+
+
+def create_delivery_trip(contact=None):
+ if not contact:
+ contact = get_contact_and_address("_Test Customer")
+
+ delivery_trip = frappe.new_doc("Delivery Trip")
+ delivery_trip.update({
+ "doctype": "Delivery Trip",
+ "company": erpnext.get_default_company(),
+ "departure_time": add_days(now_datetime(), 5),
+ "driver": frappe.db.get_value('Driver', {"full_name": "Newton Scmander"}),
+ "vehicle": "JB 007",
+ "delivery_stops": [{
+ "customer": "_Test Customer",
+ "address": contact.shipping_address.parent,
+ "contact": contact.contact_person.parent
+ },
+ {
+ "customer": "_Test Customer",
+ "address": contact.shipping_address.parent,
+ "contact": contact.contact_person.parent
+ }]
+ })
+ delivery_trip.insert()
+
+ return delivery_trip