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 frappe.utils import getdate
__version__ = '10.1.68'
__version__ = '10.1.70'
def get_default_company(user=None):
'''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
@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),

View File

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

View File

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

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')))
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],

View File

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

View File

@ -345,6 +345,7 @@ 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)})
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):

View File

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

View File

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

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
$.extend(cur_frm.cscript, {
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] } };
})
}
});

View File

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

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,
"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",

View File

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

View File

@ -65,31 +65,43 @@ frappe.ui.form.on('Delivery Trip', {
},
calculate_arrival_time: function (frm) {
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: {
name: frm.doc.name,
delivery_trip: frm.doc.name,
},
callback: function (r) {
frm.reload_doc();
}
});
}
})
},
optimize_route: function (frm) {
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: {
name: frm.doc.name,
delivery_trip: frm.doc.name,
},
callback: function (r) {
frm.reload_doc();
}
});
}
})
},
notify_customers: function (frm) {

View File

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

View File

@ -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()
@ -104,66 +247,82 @@ def get_contact_display(contact):
"Contact", contact,
["first_name", "last_name", "phone", "mobile_no"],
as_dict=1)
contact_info.html = """ <b>%(first_name)s %(last_name)s</b> <br> %(phone)s <br> %(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('<br>')
# 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))

View File

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