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