Merge branch 'staging-fixes' into staging

This commit is contained in:
Frappe Bot 2018-11-09 10:19:21 +00:00
commit 35ec2469e9
19 changed files with 916 additions and 247 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '10.1.68' __version__ = '10.1.70'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -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 return paid_amount[0][0] if paid_amount else 0
@frappe.whitelist() @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({ return frappe._dict({
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center), "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), "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),

View File

@ -201,8 +201,8 @@ def get_pricing_rule_for_item(args):
"discount_percentage": 0.0 "discount_percentage": 0.0
}) })
else: 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'): elif args.get('pricing_rule'):
item_details = remove_pricing_rule_for_item(args.get("pricing_rule"), item_details) 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.selling = 1 if doctype == "Customer" else 0
doc.buying = 1 if doctype == "Supplier" else 0 doc.buying = 1 if doctype == "Supplier" else 0
return doc return doc

View File

@ -662,9 +662,6 @@ class SalesInvoice(SellingController):
def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False): 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) auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if not self.grand_total:
return
if not gl_entries: if not gl_entries:
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()

View File

@ -21,6 +21,8 @@ def get_data(filters, show_party_name):
party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type')))
if filters.get('party_type') == 'Student': if filters.get('party_type') == 'Student':
party_name_field = 'first_name' 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 {} party_filters = {"name": filters.get("party")} if filters.get("party") else {}
parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field], parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field],

View File

@ -117,6 +117,13 @@ class AccountsController(TransactionBase):
if self.get("group_same_items"): if self.get("group_same_items"):
self.group_similar_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): def validate_paid_amount(self):
if hasattr(self, "is_pos") or hasattr(self, "is_paid"): if hasattr(self, "is_pos") or hasattr(self, "is_paid"):
is_paid = self.get("is_pos") or self.get("is_paid") is_paid = self.get("is_pos") or self.get("is_paid")

View File

@ -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)])) sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
if sales_orders: if sales_orders:
po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', 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): def validate_items(self):
# validate items to see if they have is_sales_item enabled # validate items to see if they have is_sales_item enabled

View File

@ -11,8 +11,8 @@ app_email = "info@erpnext.com"
app_license = "GNU General Public License (v3)" app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext" source_link = "https://github.com/frappe/erpnext"
develop_version = '11.x.x-develop' develop_version = '12.x.x-develop'
staging_version = '11.0.3-beta.19' staging_version = '11.0.3-beta.20'
error_report_email = "support@erpnext.com" error_report_email = "support@erpnext.com"

View File

@ -54,7 +54,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "eval:!doc.__islocal", "depends_on": "",
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
@ -1976,7 +1976,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-10-11 11:52:39.047935", "modified": "2018-10-24 02:07:21.618275",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@ -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 // License: GNU General Public License v3. See license.txt
$.extend(cur_frm.cscript, { $.extend(cur_frm.cscript, {
validate: function(doc, cdt, cdn) { onload: function (doc, cdt, cdn) {
return $c_obj(doc, 'get_defaults', '', function(r, rt){ 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; 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] } };
})
} }
}); });

View File

@ -1,122 +1,183 @@
{ {
"allow_copy": 1, "allow_copy": 1,
"allow_email": 0, "allow_guest_to_view": 0,
"allow_import": 0, "allow_import": 0,
"allow_print": 0,
"allow_rename": 0, "allow_rename": 0,
"allow_trash": 0, "beta": 0,
"creation": "2013-05-02 17:53:24", "creation": "2013-05-02 17:53:24",
"custom": 0, "custom": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 0,
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "default_company", "fieldname": "default_company",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Company", "label": "Default Company",
"length": 0, "length": 0,
"no_column": 0,
"no_copy": 0, "no_copy": 0,
"options": "Company", "options": "Company",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0,
"unique": 0 "unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "current_fiscal_year", "fieldname": "current_fiscal_year",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Fiscal Year", "label": "Current Fiscal Year",
"length": 0, "length": 0,
"no_column": 0,
"no_copy": 0, "no_copy": 0,
"options": "Fiscal Year", "options": "Fiscal Year",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0,
"unique": 0 "unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "country", "fieldname": "country",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Country", "label": "Country",
"length": 0, "length": 0,
"no_column": 0,
"no_copy": 0, "no_copy": 0,
"options": "Country", "options": "Country",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0,
"unique": 0 "unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 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", "fieldname": "column_break_8",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"length": 0, "length": 0,
"no_column": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0,
"unique": 0 "unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"default": "INR", "default": "INR",
"fieldname": "default_currency", "fieldname": "default_currency",
"fieldtype": "Link", "fieldtype": "Link",
@ -124,26 +185,32 @@
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Default Currency", "label": "Default Currency",
"length": 0, "length": 0,
"no_column": 0,
"no_copy": 0, "no_copy": 0,
"options": "Currency", "options": "Currency",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0,
"unique": 0 "unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"description": "Do not show any symbol like $ etc next to currencies.", "description": "Do not show any symbol like $ etc next to currencies.",
"fieldname": "hide_currency_symbol", "fieldname": "hide_currency_symbol",
"fieldtype": "Select", "fieldtype": "Select",
@ -151,26 +218,32 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Hide Currency Symbol", "label": "Hide Currency Symbol",
"length": 0, "length": 0,
"no_column": 0,
"no_copy": 0, "no_copy": 0,
"options": "\nNo\nYes", "options": "\nNo\nYes",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0,
"unique": 0 "unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"description": "If disable, 'Rounded Total' field will not be visible in any transaction", "description": "If disable, 'Rounded Total' field will not be visible in any transaction",
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
@ -178,25 +251,31 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Disable Rounded Total", "label": "Disable Rounded Total",
"length": 0, "length": 0,
"no_column": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0,
"unique": 0 "unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"description": "If disable, 'In Words' field will not be visible in any transaction", "description": "If disable, 'In Words' field will not be visible in any transaction",
"fieldname": "disable_in_words", "fieldname": "disable_in_words",
"fieldtype": "Check", "fieldtype": "Check",
@ -204,7 +283,9 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Disable In Words", "label": "Disable In Words",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
@ -213,26 +294,28 @@
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0,
"unique": 0 "unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0, "hide_heading": 0,
"hide_toolbar": 0, "hide_toolbar": 0,
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 1, "in_create": 1,
"is_submittable": 0, "is_submittable": 0,
"is_transaction_doc": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"menu_index": 0, "menu_index": 0,
"modified": "2016-03-03 16:14:41.260467", "modified": "2018-10-15 03:08:19.886212",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Global Defaults", "name": "Global Defaults",
@ -240,7 +323,6 @@
"permissions": [ "permissions": [
{ {
"amend": 0, "amend": 0,
"apply_user_permissions": 0,
"cancel": 0, "cancel": 0,
"create": 1, "create": 1,
"delete": 0, "delete": 0,
@ -252,7 +334,6 @@
"print": 0, "print": 0,
"read": 1, "read": 1,
"report": 0, "report": 0,
"restrict": 0,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0, "set_user_permissions": 0,
"share": 1, "share": 1,
@ -260,10 +341,12 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 1, "read_only": 1,
"read_only_onload": 0, "read_only_onload": 0,
"show_in_menu": 0, "show_name_in_global_search": 0,
"sort_order": "DESC", "sort_order": "DESC",
"use_template": 0, "track_changes": 0,
"version": 0 "track_seen": 0,
"track_views": 0
} }

View File

@ -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()
]);
});

View File

@ -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

View File

@ -143,6 +143,70 @@
"set_only_once": 0, "set_only_once": 0,
"translatable": 0, "translatable": 0,
"unique": 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, "has_web_view": 0,
@ -155,7 +219,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-09-05 00:16:23.569855", "modified": "2018-09-09 23:51:34.279941",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Settings", "name": "Delivery Settings",

View File

@ -493,6 +493,38 @@
"translatable": 0, "translatable": 0,
"unique": 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_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -525,6 +557,168 @@
"translatable": 0, "translatable": 0,
"unique": 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_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -568,7 +762,7 @@
"issingle": 0, "issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-09-05 00:51:55.275009", "modified": "2018-10-11 22:32:27.450906",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Stop", "name": "Delivery Stop",

View File

@ -65,31 +65,43 @@ frappe.ui.form.on('Delivery Trip', {
}, },
calculate_arrival_time: function (frm) { calculate_arrival_time: function (frm) {
frappe.call({ frappe.db.get_value("Google Maps Settings", { name: "Google Maps Settings" }, "enabled", (r) => {
method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.get_arrival_times', if (r.enabled == 0) {
freeze: true, frappe.throw(__("Please enable Google Maps Settings to estimate and optimize routes"));
freeze_message: __("Updating estimated arrival times."), } else {
args: { frappe.call({
name: frm.doc.name, method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.get_arrival_times',
}, freeze: true,
callback: function (r) { freeze_message: __("Updating estimated arrival times."),
frm.reload_doc(); args: {
delivery_trip: frm.doc.name,
},
callback: function (r) {
frm.reload_doc();
}
});
} }
}); })
}, },
optimize_route: function (frm) { optimize_route: function (frm) {
frappe.call({ frappe.db.get_value("Google Maps Settings", {name: "Google Maps Settings"}, "enabled", (r) => {
method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.optimize_route', if (r.enabled == 0) {
freeze: true, frappe.throw(__("Please enable Google Maps Settings to estimate and optimize routes"));
freeze_message: __("Optimizing routes."), } else {
args: { frappe.call({
name: frm.doc.name, method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.optimize_route',
}, freeze: true,
callback: function (r) { freeze_message: __("Optimizing routes."),
frm.reload_doc(); args: {
delivery_trip: frm.doc.name,
},
callback: function (r) {
frm.reload_doc();
}
});
} }
}); })
}, },
notify_customers: function (frm) { notify_customers: function (frm) {

View File

@ -159,101 +159,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"length": 0, "label": "Delivery Details",
"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,
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@ -336,6 +242,104 @@
"translatable": 0, "translatable": 0,
"unique": 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_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -369,6 +373,38 @@
"translatable": 0, "translatable": 0,
"unique": 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_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -441,7 +477,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "eval:!cur_frm.is_new()", "depends_on": "eval:!doc.__islocal",
"fieldname": "calculate_arrival_time", "fieldname": "calculate_arrival_time",
"fieldtype": "Button", "fieldtype": "Button",
"hidden": 0, "hidden": 0,
@ -474,6 +510,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "eval:!doc.__islocal",
"fieldname": "optimize_route", "fieldname": "optimize_route",
"fieldtype": "Button", "fieldtype": "Button",
"hidden": 0, "hidden": 0,
@ -574,7 +611,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-09-05 01:20:34.165834", "modified": "2018-10-11 22:32:04.355068",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Trip", "name": "Delivery Trip",
@ -627,5 +664,6 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "track_changes": 0,
"track_seen": 0 "track_seen": 0,
"track_views": 0
} }

View File

@ -10,17 +10,43 @@ import frappe
from frappe import _ from frappe import _
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.document import Document 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): 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): def on_submit(self):
self.update_delivery_notes() self.update_delivery_notes()
def on_cancel(self): def on_cancel(self):
self.update_delivery_notes(delete=True) 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): 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])) delivery_notes = list(set([stop.delivery_note for stop in self.delivery_stops if stop.delivery_note]))
update_fields = { update_fields = {
@ -28,7 +54,7 @@ class DeliveryTrip(Document):
"driver_name": self.driver_name, "driver_name": self.driver_name,
"vehicle_no": self.vehicle, "vehicle_no": self.vehicle,
"lr_no": self.name, "lr_no": self.name,
"lr_date": self.date "lr_date": self.departure_time
} }
for delivery_note in delivery_notes: 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] delivery_notes = [get_link_to_form("Delivery Note", note) for note in delivery_notes]
frappe.msgprint(_("Delivery Notes {0} updated".format(", ".join(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): def get_default_contact(out, name):
contact_persons = frappe.db.sql( contact_persons = frappe.db.sql(
""" """
select parent, SELECT parent,
(select is_primary_contact from tabContact c where c.name = dl.parent) (SELECT is_primary_contact FROM tabContact c WHERE c.name = dl.parent) AS is_primary_contact
as is_primary_contact FROM
from
`tabDynamic Link` dl `tabDynamic Link` dl
where WHERE
dl.link_doctype="Customer" and dl.link_doctype="Customer"
dl.link_name=%s and AND dl.link_name=%s
dl.parenttype = 'Contact' AND dl.parenttype = "Contact"
""", (name), as_dict=1) """, (name), as_dict=1)
if contact_persons: if contact_persons:
for out.contact_person in contact_persons: for out.contact_person in contact_persons:
if out.contact_person.is_primary_contact: if out.contact_person.is_primary_contact:
return out.contact_person return out.contact_person
out.contact_person = contact_persons[0] out.contact_person = contact_persons[0]
return out.contact_person return out.contact_person
else:
return None
def get_default_address(out, name): def get_default_address(out, name):
shipping_addresses = frappe.db.sql( shipping_addresses = frappe.db.sql(
""" """
select parent, SELECT parent,
(select is_shipping_address from tabAddress a where a.name=dl.parent) as is_shipping_address (SELECT is_shipping_address FROM tabAddress a WHERE a.name=dl.parent) AS is_shipping_address
from `tabDynamic Link` dl FROM
where link_doctype="Customer" `tabDynamic Link` dl
and link_name=%s WHERE
and parenttype = 'Address' dl.link_doctype="Customer"
AND dl.link_name=%s
AND dl.parenttype = "Address"
""", (name), as_dict=1) """, (name), as_dict=1)
if shipping_addresses: if shipping_addresses:
for out.shipping_address in shipping_addresses: for out.shipping_address in shipping_addresses:
if out.shipping_address.is_shipping_address: if out.shipping_address.is_shipping_address:
return out.shipping_address return out.shipping_address
out.shipping_address = shipping_addresses[0] out.shipping_address = shipping_addresses[0]
return out.shipping_address 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() @frappe.whitelist()
@ -103,67 +246,83 @@ def get_contact_display(contact):
contact_info = frappe.db.get_value( contact_info = frappe.db.get_value(
"Contact", contact, "Contact", contact,
["first_name", "last_name", "phone", "mobile_no"], ["first_name", "last_name", "phone", "mobile_no"],
as_dict=1) as_dict=1)
contact_info.html = """ <b>%(first_name)s %(last_name)s</b> <br> %(phone)s <br> %(mobile_no)s""" % { contact_info.html = """ <b>%(first_name)s %(last_name)s</b> <br> %(phone)s <br> %(mobile_no)s""" % {
"first_name": contact_info.first_name, "first_name": contact_info.first_name,
"last_name": contact_info.last_name or "", "last_name": contact_info.last_name or "",
"phone": contact_info.phone or "", "phone": contact_info.phone or "",
"mobile_no": contact_info.mobile_no or "", "mobile_no": contact_info.mobile_no or ""
} }
return contact_info.html return contact_info.html
def process_route(name, optimize): @frappe.whitelist()
doc = frappe.get_doc("Delivery Trip", name) def optimize_route(delivery_trip):
settings = frappe.get_single("Google Maps Settings") delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
gmaps_client = settings.get_client() 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()) @frappe.whitelist()
address_list = [] 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 def sanitize_address(address):
departure_datetime = get_datetime(doc.date) + doc.departure_time """
Remove HTML breaks in a given address
try: Args:
directions = gmaps_client.directions(origin=home_address, address (str): Address to be sanitized
destination=home_address, waypoints=address_list,
optimize_waypoints=optimize, departure_time=departure_datetime)
except Exception as e:
frappe.throw((e.message))
if not directions: Returns:
(str): Sanitized address
"""
if not address:
return return
directions = directions[0] address = address.split('<br>')
duration = 0
# Google Maps returns the optimized order of the waypoints that were sent # Only get the first 3 blocks of the address
for idx, order in enumerate(directions.get("waypoint_order")): return ', '.join(address[:3])
# 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()
@frappe.whitelist() def get_directions(route, optimize):
def optimize_route(name): """
process_route(name, optimize=True) 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() Args:
def get_arrival_times(name): route (list of str): Route addresses (origin -> waypoint(s), if any -> destination)
process_route(name, optimize=False) 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() @frappe.whitelist()
@ -171,10 +330,6 @@ def notify_customers(delivery_trip):
delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip) delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
context = delivery_trip.as_dict() 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: if delivery_trip.driver:
context.update(frappe.db.get_value("Driver", delivery_trip.driver, "cell_number", as_dict=1)) context.update(frappe.db.get_value("Driver", delivery_trip.driver, "cell_number", as_dict=1))

View File

@ -9,7 +9,7 @@ import erpnext
import frappe import frappe
from erpnext.stock.doctype.delivery_trip.delivery_trip import get_contact_and_address, notify_customers 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 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): class TestDeliveryTrip(unittest.TestCase):
@ -19,28 +19,58 @@ class TestDeliveryTrip(unittest.TestCase):
create_delivery_notification() create_delivery_notification()
create_test_contact_and_address() create_test_contact_and_address()
def test_delivery_trip(self): settings = frappe.get_single("Google Maps Settings")
contact = get_contact_and_address("_Test Customer") settings.home_address = frappe.get_last_doc("Address").name
settings.save()
if not frappe.db.exists("Delivery Trip", "TOUR-00000"): self.delivery_trip = create_delivery_trip()
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()
notify_customers(delivery_trip=delivery_trip.name) def tearDown(self):
delivery_trip.load_from_db() frappe.db.sql("delete from `tabDriver`")
self.assertEqual(delivery_trip.email_notification_sent, 1) 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(): def create_driver():
@ -67,6 +97,7 @@ def create_delivery_notification():
delivery_settings = frappe.get_single("Delivery Settings") delivery_settings = frappe.get_single("Delivery Settings")
delivery_settings.dispatch_template = 'Delivery Notification' delivery_settings.dispatch_template = 'Delivery Notification'
delivery_settings.save()
def create_vehicle(): def create_vehicle():
@ -84,3 +115,30 @@ def create_vehicle():
"vehicle_value": frappe.utils.flt(500000) "vehicle_value": frappe.utils.flt(500000)
}) })
vehicle.insert() 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