From 075f6ea0d56fe6a41fb5c5e50679390994796903 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Nov 2019 19:41:04 +0530 Subject: [PATCH 01/76] feat: Provision to fetch items from BOM in Stock Entry Items are populated in child table and amounts are calculated Warehouse Fields in popup toggle based on Stock Entry Type --- erpnext/manufacturing/doctype/bom/bom.py | 1 + .../stock/doctype/stock_entry/stock_entry.js | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 225ae29429e..d02dd59d13c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -599,6 +599,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite item.image, bom.project, item.stock_uom, + item.item_group, item.allow_alternative_item, item_default.default_warehouse, item_default.expense_account as expense_account, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 0b023024f9d..cee09e78c6e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -181,6 +181,12 @@ frappe.ui.form.on('Stock Entry', { } } + if (frm.doc.docstatus === 0) { + frm.add_custom_button(__('Bill of Materials'), function(){ + frm.events.get_items_from_bom(frm); + }, __("Get items from")); + } + if (frm.doc.docstatus===0) { frm.add_custom_button(__('Purchase Invoice'), function() { erpnext.utils.map_current_doc({ @@ -387,6 +393,73 @@ frappe.ui.form.on('Stock Entry', { } }, + get_items_from_bom: function(frm) { + let filters = function(){ + return {filters: { docstatus:1 }}; + } + + let fields = [ + {"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"), + options:"BOM", reqd: 1, get_query: filters()}, + {"fieldname":"source_warehouse", "fieldtype":"Link", "label":__("Source Warehouse"), + options:"Warehouse"}, + {"fieldname":"target_warehouse", "fieldtype":"Link", "label":__("Target Warehouse"), + options:"Warehouse"}, + {"fieldname":"qty", "fieldtype":"Float", "label":__("Quantity"), + reqd: 1, "default": 1}, + {"fieldname":"fetch_exploded", "fieldtype":"Check", + "label":__("Fetch exploded BOM (including sub-assemblies)"), "default":1}, + {"fieldname":"fetch", "label":__("Get Items from BOM"), "fieldtype":"Button"} + ] + if (frm.doc.stock_entry_type == 'Material Issue'){ + fields.splice(2,1); + } + else if(frm.doc.stock_entry_type == 'Material Receipt'){ + fields.splice(1,1); + } + + let d = new frappe.ui.Dialog({ + title: __("Get Items from BOM"), + fields: fields + }); + d.get_input("fetch").on("click", function() { + let values = d.get_values(); + if(!values) return; + values["company"] = frm.doc.company; + if(!frm.doc.company) frappe.throw(__("Company field is required")); + frappe.call({ + method: "erpnext.manufacturing.doctype.bom.bom.get_bom_items", + args: values, + callback: function(r) { + if (!r.message) { + frappe.throw(__("BOM does not contain any stock item")); + } else { + erpnext.utils.remove_empty_first_row(frm, "items"); + $.each(r.message, function(i, item) { + let d = frappe.model.add_child(cur_frm.doc, "Stock Entry Detail", "items"); + d.item_code = item.item_code; + d.item_name = item.item_name; + d.item_group = item.item_group; + d.s_warehouse = values.source_warehouse; + d.t_warehouse = values.target_warehouse; + d.uom = item.stock_uom; + d.stock_uom = item.stock_uom; + d.conversion_factor = 1; + d.qty = item.qty; + d.expense_account = item.expense_account; + d.project = item.project; + frm.events.set_basic_rate(frm, d.doctype, d.name); + }); + } + d.hide(); + refresh_field("items"); + } + }); + + }); + d.show(); + }, + calculate_basic_amount: function(frm, item) { item.basic_amount = flt(flt(item.transfer_qty) * flt(item.basic_rate), precision("basic_amount", item)); From a076bddd833e6ec82fbfdf87cbbe0bbee3147368 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 15 Nov 2019 12:03:06 +0530 Subject: [PATCH 02/76] fix: Show 'Bill of Materials' custom button conditionally --- .../stock/doctype/stock_entry/stock_entry.js | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index cee09e78c6e..bb85ef0c484 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -181,12 +181,6 @@ frappe.ui.form.on('Stock Entry', { } } - if (frm.doc.docstatus === 0) { - frm.add_custom_button(__('Bill of Materials'), function(){ - frm.events.get_items_from_bom(frm); - }, __("Get items from")); - } - if (frm.doc.docstatus===0) { frm.add_custom_button(__('Purchase Invoice'), function() { erpnext.utils.map_current_doc({ @@ -257,6 +251,17 @@ frappe.ui.form.on('Stock Entry', { frm.trigger("setup_quality_inspection"); }, + stock_entry_type: function(frm){ + frm.remove_custom_button('Bill of Materials', "Get items from"); + + if (frm.doc.docstatus === 0 && ['Material Issue','Material Receipt', + 'Material Transfer','Send to Subcontractor'].includes(frm.doc.purpose)) { + frm.add_custom_button(__('Bill of Materials'), function(){ + frm.events.get_items_from_bom(frm); + }, __("Get items from")); + } + }, + purpose: function(frm) { frm.trigger('validate_purpose_consumption'); frm.fields_dict.items.grid.refresh(); @@ -411,10 +416,10 @@ frappe.ui.form.on('Stock Entry', { "label":__("Fetch exploded BOM (including sub-assemblies)"), "default":1}, {"fieldname":"fetch", "label":__("Get Items from BOM"), "fieldtype":"Button"} ] - if (frm.doc.stock_entry_type == 'Material Issue'){ + if (frm.doc.purpose == 'Material Issue'){ fields.splice(2,1); } - else if(frm.doc.stock_entry_type == 'Material Receipt'){ + else if(frm.doc.purpose == 'Material Receipt'){ fields.splice(1,1); } From e537cda0620f1be820afc9328857160693332d1a Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 20 Nov 2019 12:38:58 +0530 Subject: [PATCH 03/76] fix: Added conditional check for conversion factor --- erpnext/stock/doctype/stock_entry/stock_entry.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index bb85ef0c484..6c82c8b6b2c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -254,11 +254,11 @@ frappe.ui.form.on('Stock Entry', { stock_entry_type: function(frm){ frm.remove_custom_button('Bill of Materials', "Get items from"); - if (frm.doc.docstatus === 0 && ['Material Issue','Material Receipt', - 'Material Transfer','Send to Subcontractor'].includes(frm.doc.purpose)) { - frm.add_custom_button(__('Bill of Materials'), function(){ - frm.events.get_items_from_bom(frm); - }, __("Get items from")); + if (frm.doc.docstatus === 0 && + ['Material Issue', 'Material Receipt', 'Material Transfer', 'Send to Subcontractor'].includes(frm.doc.purpose)) { + frm.add_custom_button(__('Bill of Materials'), function() { + frm.events.get_items_from_bom(frm); + }, __("Get items from")); } }, @@ -449,7 +449,7 @@ frappe.ui.form.on('Stock Entry', { d.t_warehouse = values.target_warehouse; d.uom = item.stock_uom; d.stock_uom = item.stock_uom; - d.conversion_factor = 1; + d.conversion_factor = item.conversion_factor ? item.conversion_factor : 1; d.qty = item.qty; d.expense_account = item.expense_account; d.project = item.project; From 737d8a1e260027a16cc1e6bb5d42717372dbec1d Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 25 Nov 2019 09:42:08 +0530 Subject: [PATCH 04/76] fix: Added comments --- erpnext/stock/doctype/stock_entry/stock_entry.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6c82c8b6b2c..96e74ccca1a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -416,9 +416,12 @@ frappe.ui.form.on('Stock Entry', { "label":__("Fetch exploded BOM (including sub-assemblies)"), "default":1}, {"fieldname":"fetch", "label":__("Get Items from BOM"), "fieldtype":"Button"} ] + + //Exclude field 'Target Warehouse' in case of Material Issue if (frm.doc.purpose == 'Material Issue'){ fields.splice(2,1); } + //Exclude field 'Source Warehouse' in case of Material Receipt else if(frm.doc.purpose == 'Material Receipt'){ fields.splice(1,1); } From 647331bbd70a8d1b7c1b47c974175bbcbb70d15e Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 25 Nov 2019 13:56:00 +0530 Subject: [PATCH 05/76] feat: add search in category --- erpnext/public/js/hub/pages/Category.vue | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/hub/pages/Category.vue b/erpnext/public/js/hub/pages/Category.vue index 3a0e6bfab83..a1d5d729cc9 100644 --- a/erpnext/public/js/hub/pages/Category.vue +++ b/erpnext/public/js/hub/pages/Category.vue @@ -3,6 +3,12 @@ class="marketplace-page" :data-page-name="page_name" > + +
{{ page_title }}
From 236140d94ccf4b836933b88023f3c3b70535349b Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 28 Nov 2019 16:36:24 +0530 Subject: [PATCH 06/76] fix: Division by zero error in Stock Entry --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f81fa683ba5..2b99f72565c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -657,7 +657,7 @@ class StockEntry(StockController): item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ - (t.amount * d.basic_amount) / total_basic_amount + (t.amount * d.basic_amount) / total_basic_amount if total_basic_amount else 0 if item_account_wise_additional_cost: for d in self.get("items"): From 2597817cdeb025caa1f4d4fb7fee49831761778a Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 28 Nov 2019 18:57:42 +0530 Subject: [PATCH 07/76] fix: add new routes to handle category wise search --- erpnext/public/js/hub/PageContainer.vue | 2 +- erpnext/public/js/hub/pages/Category.vue | 2 +- erpnext/public/js/hub/pages/Home.vue | 2 +- erpnext/public/js/hub/pages/Search.vue | 15 +++++++++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/hub/PageContainer.vue b/erpnext/public/js/hub/PageContainer.vue index f151add8d5a..54c359766d3 100644 --- a/erpnext/public/js/hub/PageContainer.vue +++ b/erpnext/public/js/hub/PageContainer.vue @@ -24,7 +24,7 @@ import NotFound from './pages/NotFound.vue'; function get_route_map() { const read_only_routes = { 'marketplace/home': Home, - 'marketplace/search/:keyword': Search, + 'marketplace/search/:category/:keyword': Search, 'marketplace/category/:category': Category, 'marketplace/item/:item': Item, 'marketplace/seller/:seller': Seller, diff --git a/erpnext/public/js/hub/pages/Category.vue b/erpnext/public/js/hub/pages/Category.vue index a1d5d729cc9..057fe8bc617 100644 --- a/erpnext/public/js/hub/pages/Category.vue +++ b/erpnext/public/js/hub/pages/Category.vue @@ -67,7 +67,7 @@ export default { }, set_search_route() { - frappe.set_route('marketplace', 'search', this.search_value); + frappe.set_route('marketplace', 'search', this.category, this.search_value); }, } } diff --git a/erpnext/public/js/hub/pages/Home.vue b/erpnext/public/js/hub/pages/Home.vue index 353656957df..aaeaa7eb7c1 100644 --- a/erpnext/public/js/hub/pages/Home.vue +++ b/erpnext/public/js/hub/pages/Home.vue @@ -98,7 +98,7 @@ export default { }, set_search_route() { - frappe.set_route('marketplace', 'search', this.search_value); + frappe.set_route('marketplace', 'search', 'All', this.search_value); }, } } diff --git a/erpnext/public/js/hub/pages/Search.vue b/erpnext/public/js/hub/pages/Search.vue index 5118a814e07..2a6088925fd 100644 --- a/erpnext/public/js/hub/pages/Search.vue +++ b/erpnext/public/js/hub/pages/Search.vue @@ -29,8 +29,10 @@ export default { return { page_name: frappe.get_route()[1], items: [], - search_value: frappe.get_route()[2], + category: frappe.get_route()[2], + search_value: frappe.get_route()[3], item_id_fieldname: 'name', + filters: {}, // Constants search_placeholder: __('Search for anything ...'), @@ -40,7 +42,7 @@ export default { computed: { page_title() { return this.items.length - ? __(`Results for "${this.search_value}"`) + ? __(`Results for ${this.search_value} in category ${this.category}`) : __('No Items found.'); } }, @@ -49,14 +51,19 @@ export default { }, methods: { get_items() { - hub.call('get_items', { keyword: this.search_value }) + if (this.category !== 'All') { + this.filters['hub_category']=this.category; + } + hub.call('get_items', { keyword: this.search_value, + filters: this.filters + }) .then((items) => { this.items = items; }) }, set_route_and_get_items() { - frappe.set_route('marketplace', 'search', this.search_value); + frappe.set_route('marketplace', 'search', this.category, this.search_value); this.get_items(); }, From 8b599f2bc9c9e6b2dad7c1c6e5c67d6404c273dc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Oct 2019 01:52:03 +0530 Subject: [PATCH 08/76] fix: capacity planning back --- .../doctype/job_card/job_card.json | 1288 ++++------------- .../doctype/job_card/job_card.py | 122 +- .../manufacturing_settings.json | 715 ++------- .../doctype/work_order/work_order.js | 1 + .../doctype/work_order/work_order.py | 66 +- .../doctype/workstation/workstation.json | 583 ++------ .../doctype/workstation/workstation.py | 15 +- .../workstation/workstation_dashboard.py | 8 +- erpnext/patches.txt | 3 +- .../set_production_capacity_in_workstation.py | 8 + 10 files changed, 747 insertions(+), 2062 deletions(-) create mode 100644 erpnext/patches/v12_0/set_production_capacity_in_workstation.py diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 39c5cce313b..2c217028d06 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -1,1071 +1,299 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "PO-JOB.#####", - "beta": 0, - "creation": "2018-07-09 17:23:29.518745", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "autoname": "naming_series:", + "creation": "2018-07-09 17:23:29.518745", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "work_order", + "bom_no", + "workstation", + "operation", + "column_break_4", + "posting_date", + "company", + "for_quantity", + "wip_warehouse", + "timing_detail", + "employee", + "time_logs", + "section_break_13", + "total_completed_qty", + "total_time_in_mins", + "column_break_15", + "section_break_8", + "items", + "more_information", + "operation_id", + "transferred_qty", + "requested_qty", + "project", + "remarks", + "column_break_20", + "status", + "job_started", + "started_time", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "work_order", - "fieldtype": "Link", - "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": "Work Order", - "length": 0, - "no_copy": 0, - "options": "Work Order", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "work_order", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Work Order", + "options": "Work Order", + "read_only": 1, + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "bom_no", - "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": "BOM No", - "length": 0, - "no_copy": 0, - "options": "BOM", - "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 - }, + "fieldname": "bom_no", + "fieldtype": "Link", + "label": "BOM No", + "options": "BOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "workstation", - "fieldtype": "Link", - "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": "Workstation", - "length": 0, - "no_copy": 0, - "options": "Workstation", - "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 - }, + "fieldname": "workstation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Workstation", + "options": "Workstation", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "operation", - "fieldtype": "Link", - "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": "Operation", - "length": 0, - "no_copy": 0, - "options": "Operation", - "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 - }, + "fieldname": "operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "options": "Operation", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 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 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fetch_if_empty": 0, - "fieldname": "posting_date", - "fieldtype": "Date", - "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": "Posting 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "company", - "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": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "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 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "for_quantity", - "fieldtype": "Float", - "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": "For Quantity", - "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 - }, + "fieldname": "for_quantity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "For Quantity", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "wip_warehouse", - "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": "WIP Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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 - }, + "fieldname": "wip_warehouse", + "fieldtype": "Link", + "label": "WIP Warehouse", + "options": "Warehouse", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "timing_detail", - "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": "Timing Detail", - "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 - }, + "fieldname": "timing_detail", + "fieldtype": "Section Break", + "label": "Timing Detail" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "employee", - "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": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "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 - }, + "fieldname": "employee", + "fieldtype": "Link", + "label": "Employee", + "options": "Employee" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "time_logs", - "fieldtype": "Table", - "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": "Time Logs", - "length": 0, - "no_copy": 0, - "options": "Job Card Time Log", - "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 - }, + "fieldname": "time_logs", + "fieldtype": "Table", + "label": "Time Logs", + "options": "Job Card Time Log" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_13", - "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, - "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 - }, + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_completed_qty", - "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 Completed Qty", - "length": 0, - "no_copy": 0, - "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 - }, + "fieldname": "total_completed_qty", + "fieldtype": "Float", + "label": "Total Completed Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_15", - "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 - }, + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_time_in_mins", - "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 Time in Mins", - "length": 0, - "no_copy": 0, - "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 - }, + "fieldname": "total_time_in_mins", + "fieldtype": "Float", + "label": "Total Time in Mins", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_8", - "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": "Raw Materials", - "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 - }, + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "Raw Materials" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "items", - "fieldtype": "Table", - "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": "Items", - "length": 0, - "no_copy": 0, - "options": "Job Card Item", - "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 - }, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Job Card Item", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "more_information", - "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 - }, + "collapsible": 1, + "fieldname": "more_information", + "fieldtype": "Section Break", + "label": "More Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "operation_id", - "fieldtype": "Data", - "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": "Operation ID", - "length": 0, - "no_copy": 0, - "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 - }, + "fieldname": "operation_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Operation ID", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "transferred_qty", - "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": "Transferred Qty", - "length": 0, - "no_copy": 0, - "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 - }, + "default": "0", + "fieldname": "transferred_qty", + "fieldtype": "Float", + "label": "Transferred Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "requested_qty", - "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": "Requested Qty", - "length": 0, - "no_copy": 0, - "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 - }, + "default": "0", + "fieldname": "requested_qty", + "fieldtype": "Float", + "label": "Requested Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "project", - "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": "Project", - "length": 0, - "no_copy": 0, - "options": "Project", - "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 - }, + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "remarks", - "fieldtype": "Small Text", - "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": "Remarks", - "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 - }, + "fieldname": "remarks", + "fieldtype": "Small Text", + "label": "Remarks" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_20", - "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 - }, + "fieldname": "column_break_20", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Open", - "fetch_if_empty": 0, - "fieldname": "status", - "fieldtype": "Select", - "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": "Status", - "length": 0, - "no_copy": 1, - "options": "Open\nWork In Progress\nMaterial Transferred\nSubmitted\nCancelled\nCompleted", - "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 - }, + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Open\nWork In Progress\nMaterial Transferred\nSubmitted\nCancelled\nCompleted", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "job_started", - "fieldtype": "Check", - "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": "Job Started", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "default": "0", + "fieldname": "job_started", + "fieldtype": "Check", + "hidden": 1, + "label": "Job Started", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "started_time", - "fieldtype": "Datetime", - "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": "Started Time", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "fieldname": "started_time", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Started Time", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Job Card", - "permlevel": 0, - "print_hide": 1, - "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 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Job Card", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "PO-JOB.#####", + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "PO-JOB.#####", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-10 17:38:37.499871", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Job Card", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "modified": "2019-10-30 01:49:19.606178", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "operation", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "operation", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 9d2e620e584..e8787ed7db0 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -4,10 +4,16 @@ from __future__ import unicode_literals import frappe +import datetime from frappe import _ -from frappe.utils import flt, time_diff_in_hours, get_datetime from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document +from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, + get_time, add_to_date, time_diff, add_days, get_datetime_str) + +from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations + +class OverlapError(frappe.ValidationError): pass class JobCard(Document): def validate(self): @@ -26,7 +32,7 @@ class JobCard(Document): data = self.get_overlap_for(d) if data: frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}") - .format(d.idx, self.name, data.name)) + .format(d.idx, self.name, data.name), OverlapError) if d.from_time and d.to_time: d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 @@ -35,27 +41,120 @@ class JobCard(Document): if d.completed_qty: self.total_completed_qty += d.completed_qty - def get_overlap_for(self, args): - existing = frappe.db.sql("""select jc.name as name from + def get_overlap_for(self, args, check_next_available_slot=False): + production_capacity = 1 + + if self.workstation: + production_capacity = frappe.get_cached_value("Workstation", + self.workstation, 'production_capacity') or 1 + validate_overlap_for = " and jc.workstation = %(workstation)s " + + if self.employee: + # override capacity for employee + production_capacity = 1 + validate_overlap_for = " and jc.employee = %(employee)s " + + extra_cond = '' + if check_next_available_slot: + extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)" + + existing = frappe.db.sql("""select jc.name as name, jctl.to_time from `tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and ( (%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or (%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or - (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time)) - and jctl.name!=%(name)s - and jc.name!=%(parent)s - and jc.docstatus < 2 - and jc.employee = %(employee)s """, + (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0} + ) + and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1} + order by jctl.to_time desc limit 1""".format(extra_cond, validate_overlap_for), { "from_time": args.from_time, "to_time": args.to_time, "name": args.name or "No Name", "parent": args.parent or "No Name", - "employee": self.employee + "employee": self.employee, + "workstation": self.workstation }, as_dict=True) + if existing and production_capacity > len(existing): + return + return existing[0] if existing else None + def schedule_time_logs(self, row): + row.remaining_time_in_mins = row.time_in_mins + while row.remaining_time_in_mins > 0: + args = frappe._dict({ + "from_time": row.planned_start_time, + "to_time": row.planned_end_time + }) + + self.validate_overlap_for_workstation(args, row) + self.check_workstation_time(row) + + def validate_overlap_for_workstation(self, args, row): + # get the last record based on the to time from the job card + data = self.get_overlap_for(args, check_next_available_slot=True) + if data: + row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations()) + + def check_workstation_time(self, row): + workstation_doc = frappe.get_cached_doc("Workstation", self.workstation) + if (not workstation_doc.working_hours or + cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime"))): + row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time, + row.planned_start_time) + + self.update_time_logs(row) + return + + start_date = getdate(row.planned_start_time) + start_time = get_time(row.planned_start_time) + + new_start_date = workstation_doc.validate_workstation_holiday(start_date) + + if new_start_date != start_date: + row.planned_start_time = datetime.datetime.combine(new_start_date, start_time) + start_date = new_start_date + + total_idx = len(workstation_doc.working_hours) + + for i, time_slot in enumerate(workstation_doc.working_hours): + workstation_start_time = datetime.datetime.combine(start_date, get_time(time_slot.start_time)) + workstation_end_time = datetime.datetime.combine(start_date, get_time(time_slot.end_time)) + + if (get_datetime(row.planned_start_time) >= workstation_start_time and + get_datetime(row.planned_start_time) <= workstation_end_time): + time_in_mins = time_diff_in_minutes(workstation_end_time, row.planned_start_time) + + # If remaining time fit in workstation time logs else split hours as per workstation time + if time_in_mins > row.remaining_time_in_mins: + row.planned_end_time = add_to_date(row.planned_start_time, + minutes=row.remaining_time_in_mins) + row.remaining_time_in_mins = 0 + else: + row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins) + row.remaining_time_in_mins -= time_in_mins + + self.update_time_logs(row) + + if total_idx != (i+1) and row.remaining_time_in_mins > 0: + row.planned_start_time = datetime.datetime.combine(start_date, + get_time(workstation_doc.working_hours[i+1].start_time)) + + if row.remaining_time_in_mins > 0: + start_date = add_days(start_date, 1) + row.planned_start_time = datetime.datetime.combine(start_date, + get_time(workstation_doc.working_hours[0].start_time)) + + def update_time_logs(self, row): + self.append("time_logs", { + "from_time": row.planned_start_time, + "to_time": row.planned_end_time, + "completed_qty": 0, + "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time), + }) + def get_required_items(self): if not self.get('work_order'): return @@ -251,3 +350,6 @@ def make_stock_entry(source_name, target_doc=None): }, target_doc, set_missing_values) return doclist + +def time_diff_in_minutes(string_ed_date, string_st_date): + return time_diff(string_ed_date, string_st_date).total_seconds() / 60 \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 461b9ab3dfa..86fa7a8901a 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -1,585 +1,178 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-11-27 14:12:07.542534", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "creation": "2014-11-27 14:12:07.542534", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "raw_materials_consumption_section", + "material_consumption", + "column_break_3", + "backflush_raw_materials_based_on", + "capacity_planning", + "disable_capacity_planning", + "allow_overtime", + "allow_production_on_holidays", + "column_break_5", + "capacity_planning_for_days", + "mins_between_operations", + "section_break_6", + "default_wip_warehouse", + "default_fg_warehouse", + "column_break_11", + "default_scrap_warehouse", + "over_production_for_sales_and_work_order_section", + "overproduction_percentage_for_sales_order", + "column_break_16", + "overproduction_percentage_for_work_order", + "other_settings_section", + "update_bom_costs_automatically" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "capacity_planning", - "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": "Capacity Planning", - "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 - }, + "fieldname": "capacity_planning", + "fieldtype": "Section Break", + "label": "Capacity Planning" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Disables creation of time logs against Work Orders. Operations shall not be tracked against Work Order", - "fieldname": "disable_capacity_planning", - "fieldtype": "Check", - "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": "Disable Capacity Planning and Time Tracking", - "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 - }, + "default": "0", + "depends_on": "eval:!doc.disable_capacity_planning", + "description": "Plan time logs outside Workstation Working Hours.", + "fieldname": "allow_overtime", + "fieldtype": "Check", + "label": "Allow Overtime" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Plan time logs outside Workstation Working Hours.", - "fieldname": "allow_overtime", - "fieldtype": "Check", - "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": "Allow Overtime", - "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 - }, + "default": "0", + "depends_on": "eval:!doc.disable_capacity_planning", + "fieldname": "allow_production_on_holidays", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Allow Production on Holidays" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "allow_production_on_holidays", - "fieldtype": "Check", - "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": "Allow Production on Holidays", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "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 - }, + "default": "30", + "depends_on": "eval:!doc.disable_capacity_planning", + "description": "Try planning operations for X days in advance.", + "fieldname": "capacity_planning_for_days", + "fieldtype": "Int", + "label": "Capacity Planning For (Days)" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "30", - "description": "Try planning operations for X days in advance.", - "fieldname": "capacity_planning_for_days", - "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": "Capacity Planning For (Days)", - "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 - }, + "depends_on": "eval:!doc.disable_capacity_planning", + "description": "Default 10 mins", + "fieldname": "mins_between_operations", + "fieldtype": "Int", + "label": "Time Between Operations (in mins)" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Default 10 mins", - "fieldname": "mins_between_operations", - "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": "Time Between Operations (in mins)", - "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 - }, + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Default Warehouses for Production" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "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, - "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 - }, + "fieldname": "overproduction_percentage_for_sales_order", + "fieldtype": "Percent", + "label": "Overproduction Percentage For Sales Order" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "overproduction_percentage_for_sales_order", - "fieldtype": "Percent", - "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": "Overproduction Percentage For Sales Order", - "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 - }, + "fieldname": "overproduction_percentage_for_work_order", + "fieldtype": "Percent", + "label": "Overproduction Percentage For Work Order" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "overproduction_percentage_for_work_order", - "fieldtype": "Percent", - "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": "Overproduction Percentage For Work Order", - "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 - }, + "default": "BOM", + "fieldname": "backflush_raw_materials_based_on", + "fieldtype": "Select", + "label": "Backflush Raw Materials Based On", + "options": "BOM\nMaterial Transferred for Manufacture" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "BOM", - "fieldname": "backflush_raw_materials_based_on", - "fieldtype": "Select", - "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": "Backflush Raw Materials Based On", - "length": 0, - "no_copy": 0, - "options": "BOM\nMaterial Transferred for Manufacture", - "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 - }, + "default": "0", + "description": "Allow multiple Material Consumption against a Work Order", + "fieldname": "material_consumption", + "fieldtype": "Check", + "label": "Allow Multiple Material Consumption" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Allow multiple Material Consumption against a Work Order", - "fieldname": "material_consumption", - "fieldtype": "Check", - "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": "Allow Multiple Material Consumption", - "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 - }, + "default": "0", + "description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.", + "fieldname": "update_bom_costs_automatically", + "fieldtype": "Check", + "label": "Update BOM Cost Automatically" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.", - "fieldname": "update_bom_costs_automatically", - "fieldtype": "Check", - "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": "Update BOM Cost Automatically", - "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 - }, + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "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 - }, + "fieldname": "default_wip_warehouse", + "fieldtype": "Link", + "label": "Default Work In Progress Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_wip_warehouse", - "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 Work In Progress Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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 - }, + "fieldname": "default_fg_warehouse", + "fieldtype": "Link", + "label": "Default Finished Goods Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_fg_warehouse", - "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 Finished Goods Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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 + "default": "0", + "fieldname": "disable_capacity_planning", + "fieldtype": "Check", + "label": "Disable Capacity Planning" + }, + { + "fieldname": "default_scrap_warehouse", + "fieldtype": "Link", + "label": "Default Scrap Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "over_production_for_sales_and_work_order_section", + "fieldtype": "Section Break", + "label": "Over Production for Sales and Work Order" + }, + { + "fieldname": "raw_materials_consumption_section", + "fieldtype": "Section Break", + "label": "Raw Materials Consumption" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "other_settings_section", + "fieldtype": "Section Break", + "label": "Other Settings" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-wrench", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-05-28 00:46:25.310621", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Manufacturing Settings", - "name_case": "", - "owner": "Administrator", + ], + "icon": "icon-wrench", + "issingle": 1, + "modified": "2019-11-26 13:10:45.569341", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing Settings", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Manufacturing Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "read": 1, + "role": "Manufacturing Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 107c79b89bc..5c721c723d2 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -551,6 +551,7 @@ erpnext.work_order = { if (!r.exe) { frm.set_value("wip_warehouse", r.message.wip_warehouse); frm.set_value("fg_warehouse", r.message.fg_warehouse); + frm.set_value("scrap_warehouse", r.message.scrap_warehouse); } } }); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2c16bbe90c5..84f570b07af 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -12,7 +12,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items from dateutil.relativedelta import relativedelta from erpnext.stock.doctype.item.item import validate_end_of_life from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError -from erpnext.projects.doctype.timesheet.timesheet import OverlapError +from erpnext.manufacturing.doctype.job_card.job_card import OverlapError from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty @@ -260,12 +260,50 @@ class WorkOrder(Document): self.update_reserved_qty_for_production() def create_job_card(self): - for row in self.operations: + manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") + + enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) + plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 + + for i, row in enumerate(self.operations): + self.set_operation_start_end_time(i, row) + if not row.workstation: frappe.throw(_("Row {0}: select the workstation against the operation {1}") .format(row.idx, row.operation)) - create_job_card(self, row, auto_create=True) + original_start_time = row.planned_start_time + job_card_doc = create_job_card(self, row, + enable_capacity_planning=enable_capacity_planning, auto_create=True) + + if enable_capacity_planning and job_card_doc: + row.planned_start_time = job_card_doc.time_logs[0].from_time + row.planned_end_time = job_card_doc.time_logs[-1].to_time + + if date_diff(row.planned_start_time, original_start_time) > plan_days: + frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") + .format(plan_days, row.operation)) + + row.db_update() + + planned_end_date = self.operations and self.operations[-1].planned_end_time + if planned_end_date: + self.db_set("planned_end_date", planned_end_date) + + def set_operation_start_end_time(self, idx, row): + """Set start and end time for given operation. If first operation, set start as + `planned_start_date`, else add time diff to end time of earlier operation.""" + if idx==0: + # first operation at planned_start date + row.planned_start_time = self.planned_start_date + else: + row.planned_start_time = get_datetime(self.operations[idx-1].planned_end_time)\ + + get_mins_between_operations() + + row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta(minutes = row.time_in_mins) + + if row.planned_start_time == row.planned_end_time: + frappe.throw(_("Capacity Planning Error, planned start time can not be same as end time")) def validate_cancel(self): if self.status == "Stopped": @@ -327,9 +365,8 @@ class WorkOrder(Document): """Fetch operations from BOM and set in 'Work Order'""" self.set('operations', []) - if not self.bom_no \ - or cint(frappe.db.get_single_value("Manufacturing Settings", "disable_capacity_planning")): - return + if not self.bom_no: + return if self.use_multi_level_bom: bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() @@ -681,11 +718,13 @@ def make_stock_entry(work_order_id, purpose, qty=None): @frappe.whitelist() def get_default_warehouse(): - wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", - "default_wip_warehouse") - fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", - "default_fg_warehouse") - return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse} + doc = frappe.get_cached_doc("Manufacturing Settings") + + return { + "wip_warehouse": doc.default_wip_warehouse, + "fg_warehouse": doc.default_fg_warehouse, + "scrap_warehouse": doc.default_scrap_warehouse + } @frappe.whitelist() def stop_unstop(work_order, status): @@ -721,7 +760,7 @@ def make_job_card(work_order, operation, workstation, qty=0): if row: return create_job_card(work_order, row, qty) -def create_job_card(work_order, row, qty=0, auto_create=False): +def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): doc = frappe.new_doc("Job Card") doc.update({ 'work_order': work_order.name, @@ -741,6 +780,9 @@ def create_job_card(work_order, row, qty=0, auto_create=False): if auto_create: doc.flags.ignore_mandatory = True + if enable_capacity_planning: + doc.schedule_time_logs(row) + doc.insert() frappe.msgprint(_("Job card {0} created").format(doc.name)) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index dca9891277f..d130391cece 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -1,466 +1,159 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:workstation_name", - "beta": 0, - "creation": "2013-01-10 16:34:17", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:workstation_name", + "creation": "2013-01-10 16:34:17", + "doctype": "DocType", + "document_type": "Setup", + "field_order": [ + "workstation_name", + "production_capacity", + "column_break_3", + "over_heads", + "hour_rate_electricity", + "hour_rate_consumable", + "column_break_11", + "hour_rate_rent", + "hour_rate_labour", + "hour_rate", + "working_hours_section", + "holiday_list", + "working_hours", + "workstaion_description", + "description" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "description_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": "Description", - "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, - "unique": 0 - }, + "fieldname": "workstation_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Workstation Name", + "oldfieldname": "workstation_name", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "workstation_name", - "fieldtype": "Data", - "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": "Workstation Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "workstation_name", - "oldfieldtype": "Data", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "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": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Text", - "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, - "unique": 0, + "fieldname": "description", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "over_heads", - "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": "Operating Costs", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "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, - "unique": 0 - }, + "fieldname": "over_heads", + "fieldtype": "Section Break", + "label": "Operating Costs", + "oldfieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "per hour", - "fieldname": "hour_rate_electricity", - "fieldtype": "Currency", - "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": "Electricity Cost", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate_electricity", - "oldfieldtype": "Currency", - "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, - "unique": 0 - }, + "bold": 1, + "description": "per hour", + "fieldname": "hour_rate_electricity", + "fieldtype": "Currency", + "label": "Electricity Cost", + "oldfieldname": "hour_rate_electricity", + "oldfieldtype": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "per hour", - "fieldname": "hour_rate_consumable", - "fieldtype": "Currency", - "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": "Consumable Cost", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate_consumable", - "oldfieldtype": "Currency", - "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, - "unique": 0 - }, + "bold": 1, + "description": "per hour", + "fieldname": "hour_rate_consumable", + "fieldtype": "Currency", + "label": "Consumable Cost", + "oldfieldname": "hour_rate_consumable", + "oldfieldtype": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "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, - "unique": 0 - }, + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "per hour", - "fieldname": "hour_rate_rent", - "fieldtype": "Currency", - "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": "Rent Cost", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate_rent", - "oldfieldtype": "Currency", - "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, - "unique": 0 - }, + "bold": 1, + "description": "per hour", + "fieldname": "hour_rate_rent", + "fieldtype": "Currency", + "label": "Rent Cost", + "oldfieldname": "hour_rate_rent", + "oldfieldtype": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Wages per hour", - "fieldname": "hour_rate_labour", - "fieldtype": "Currency", - "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": "Wages", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate_labour", - "oldfieldtype": "Currency", - "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, - "unique": 0 - }, + "bold": 1, + "description": "Wages per hour", + "fieldname": "hour_rate_labour", + "fieldtype": "Currency", + "label": "Wages", + "oldfieldname": "hour_rate_labour", + "oldfieldtype": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "per hour", - "fieldname": "hour_rate", - "fieldtype": "Currency", - "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": "Net Hour Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate", - "oldfieldtype": "Currency", - "permlevel": 0, - "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, - "unique": 0 - }, + "description": "per hour", + "fieldname": "hour_rate", + "fieldtype": "Currency", + "label": "Net Hour Rate", + "oldfieldname": "hour_rate", + "oldfieldtype": "Currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "working_hours_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": "Working Hours", - "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, - "unique": 0 - }, + "fieldname": "working_hours_section", + "fieldtype": "Section Break", + "label": "Working Hours" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "working_hours", - "fieldtype": "Table", - "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": "Working Hours", - "length": 0, - "no_copy": 0, - "options": "Workstation Working Hour", - "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, - "unique": 0 - }, + "fieldname": "working_hours", + "fieldtype": "Table", + "label": "Working Hours", + "options": "Workstation Working Hour" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "holiday_list", - "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": "Holiday List", - "length": 0, - "no_copy": 0, - "options": "Holiday List", - "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, - "unique": 0 + "fieldname": "holiday_list", + "fieldtype": "Link", + "label": "Holiday List", + "options": "Holiday List" + }, + { + "default": "1", + "fieldname": "production_capacity", + "fieldtype": "Int", + "label": "Production Capacity", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "workstaion_description", + "fieldtype": "Section Break", + "label": "Description" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-wrench", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-07-18 22:28:50.163219", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Workstation", - "owner": "Administrator", + ], + "icon": "icon-wrench", + "idx": 1, + "modified": "2019-11-26 12:39:19.742052", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 1, + "show_name_in_global_search": 1, + "sort_order": "ASC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index f6ab72ade49..3512e590458 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -4,7 +4,9 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, cint, getdate, formatdate, comma_and, time_diff_in_seconds, to_timedelta +from erpnext.support.doctype.issue.issue import get_holidays +from frappe.utils import (flt, cint, getdate, formatdate, + comma_and, time_diff_in_seconds, to_timedelta, add_days) from frappe.model.document import Document from dateutil.parser import parse @@ -43,6 +45,17 @@ class Workstation(Document): where parent = %s and workstation = %s""", (self.hour_rate, bom_no[0], self.name)) + def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False): + if not skip_holiday_list_check and (not self.holiday_list or + cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"))): + return schedule_date + + if schedule_date in tuple(get_holidays(self.holiday_list)): + schedule_date = add_days(schedule_date, 1) + self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True) + + return schedule_date + @frappe.whitelist() def get_default_holiday_list(): return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list") diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py index 9e0d1d17394..7f0124b5030 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py +++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py @@ -6,8 +6,12 @@ def get_data(): 'fieldname': 'workstation', 'transactions': [ { - 'label': _('Manufacture'), - 'items': ['BOM', 'Routing', 'Work Order', 'Job Card', 'Operation', 'Timesheet'] + 'label': _('Master'), + 'items': ['BOM', 'Routing', 'Operation'] + }, + { + 'label': _('Transaction'), + 'items': ['Work Order', 'Job Card', 'Timesheet'] } ] } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 07b646b0f82..daedca7a28c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -646,4 +646,5 @@ erpnext.patches.v12_0.set_payment_entry_status erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger -erpnext.patches.v12_0.update_price_or_product_discount \ No newline at end of file +erpnext.patches.v12_0.update_price_or_product_discount +erpnext.patches.v12_0.set_production_capacity_in_workstation \ No newline at end of file diff --git a/erpnext/patches/v12_0/set_production_capacity_in_workstation.py b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py new file mode 100644 index 00000000000..bae1e28deb9 --- /dev/null +++ b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "workstation") + + frappe.db.sql(""" UPDATE `tabWorkstation` + SET production_capacity = 1 """) \ No newline at end of file From 92b9d7383d246ceca8447a1afe7e9712279b76a7 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 29 Nov 2019 14:22:52 +0530 Subject: [PATCH 09/76] fix: show create payment request for so that are not billed --- erpnext/accounts/doctype/payment_request/payment_request.py | 4 ++-- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index eda59abf04e..6133b1ccd4b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -350,13 +350,13 @@ def get_amount(ref_doc): if dt in ["Sales Order", "Purchase Order"]: grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) - if dt in ["Sales Invoice", "Purchase Invoice"]: + elif dt in ["Sales Invoice", "Purchase Invoice"]: if ref_doc.party_account_currency == ref_doc.currency: grand_total = flt(ref_doc.outstanding_amount) else: grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate - if dt == "Fees": + elif dt == "Fees": grand_total = ref_doc.outstanding_amount if grand_total > 0 : diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 85e81436d16..7dc58b582ac 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -202,7 +202,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } // payment request - if(flt(doc.per_billed)==0) { + if(flt(doc.per_billed)<100) { this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create')); this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create')); } From 611d2ccd04edf186718704b60f6f0f125c3f9e32 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 28 Nov 2019 10:11:03 +0530 Subject: [PATCH 10/76] chore: updated path for set_request in v13-develop --- erpnext/portal/doctype/homepage/test_homepage.py | 2 +- .../portal/doctype/homepage_section/test_homepage_section.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/portal/doctype/homepage/test_homepage.py b/erpnext/portal/doctype/homepage/test_homepage.py index b262c4640cc..bf5c4025a0b 100644 --- a/erpnext/portal/doctype/homepage/test_homepage.py +++ b/erpnext/portal/doctype/homepage/test_homepage.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.tests.test_website import set_request +from frappe.utils import set_request from frappe.website.render import render class TestHomepage(unittest.TestCase): diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py index c52b7a96c65..5b3196def25 100644 --- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py +++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest from bs4 import BeautifulSoup -from frappe.tests.test_website import set_request +from frappe.utils import set_request from frappe.website.render import render class TestHomepageSection(unittest.TestCase): From 8c6bc34bc4204e19f9712c6df25e71acae0b6ffb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 29 Nov 2019 18:42:20 +0530 Subject: [PATCH 11/76] added test cases --- .../doctype/work_order/test_work_order.py | 46 ++++++++++++++++--- .../doctype/work_order/work_order.py | 3 +- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index ea2e7a96e1d..0a8f41fc49f 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -5,10 +5,10 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import flt, time_diff_in_hours, now, add_days, cint +from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.manufacturing.doctype.work_order.work_order \ - import make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError +from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, + ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.utils import get_bin from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -307,14 +307,50 @@ class TestWorkOrder(unittest.TestCase): {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) if data: + frappe.db.set_value("Manufacturing Settings", + None, "disable_capacity_planning", 0) + bom, bom_item = data bom_doc = frappe.get_doc('BOM', bom) work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + self.assertTrue(work_order.planned_end_date) job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) self.assertEqual(len(job_cards), len(bom_doc.operations)) + def test_capcity_planning(self): + frappe.db.set_value("Manufacturing Settings", None, { + "disable_capacity_planning": 0, + "capacity_planning_for_days": 1 + }) + + data = frappe.get_cached_value('BOM', {'docstatus': 1, 'item': '_Test FG Item 2', + 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + planned_start_date = add_months(today(), months=-1) + work_order = make_wo_order_test_record(item=bom_item, + qty=10, bom_no=bom, planned_start_date=planned_start_date) + + work_order1 = make_wo_order_test_record(item=bom_item, + qty=30, bom_no=bom, planned_start_date=planned_start_date, do_not_submit=1) + + self.assertRaises(CapacityError, work_order1.submit) + + frappe.db.set_value("Manufacturing Settings", None, { + "capacity_planning_for_days": 30 + }) + + work_order1.reload() + work_order1.submit() + self.assertTrue(work_order1.docstatus, 1) + + work_order1.cancel() + work_order.cancel() + def test_work_order_with_non_transfer_item(self): items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0} for item, allow_transfer in items.items(): @@ -371,14 +407,12 @@ def make_wo_order_test_record(**args): wo_order.skip_transfer=1 wo_order.get_items_and_operations_from_bom() wo_order.sales_order = args.sales_order or None + wo_order.planned_start_date = args.planned_start_date or now() if args.source_warehouse: for item in wo_order.get("required_items"): item.source_warehouse = args.source_warehouse - if args.planned_start_date: - wo_order.planned_start_date = args.planned_start_date - if not args.do_not_save: wo_order.insert() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 84f570b07af..ff489542f66 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -22,6 +22,7 @@ from erpnext.utilities.transaction_base import validate_uom_is_integer from frappe.model.mapper import get_mapped_doc class OverProductionError(frappe.ValidationError): pass +class CapacityError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass @@ -282,7 +283,7 @@ class WorkOrder(Document): if date_diff(row.planned_start_time, original_start_time) > plan_days: frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") - .format(plan_days, row.operation)) + .format(plan_days, row.operation), CapacityError) row.db_update() From 23b1dae6a7218df6a4220e905dbebcf00488aecb Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 29 Nov 2019 20:22:06 +0530 Subject: [PATCH 12/76] chore: dropped dead setup_wizard test fix: fixed imports for teste utils --- .../test_product_configurator.py | 2 +- .../setup/setup_wizard/test_setup_wizard.py | 71 ------------------- 2 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 erpnext/setup/setup_wizard/test_setup_wizard.py diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index a534e5f8382..97042dba92c 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from bs4 import BeautifulSoup import frappe, unittest -from frappe.tests.test_website import set_request, get_html_for_route +from frappe.utils import set_request, get_html_for_route from frappe.website.render import render from erpnext.portal.product_configurator.utils import get_products_for_website from erpnext.stock.doctype.item.test_item import make_item_variant diff --git a/erpnext/setup/setup_wizard/test_setup_wizard.py b/erpnext/setup/setup_wizard/test_setup_wizard.py deleted file mode 100644 index a489133abae..00000000000 --- a/erpnext/setup/setup_wizard/test_setup_wizard.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals - -import frappe, time -from frappe.utils.selenium_testdriver import TestDriver - -def run_setup_wizard_test(): - driver = TestDriver() - frappe.db.set_default('in_selenium', '1') - frappe.db.commit() - - driver.login('#page-setup-wizard') - print('Running Setup Wizard Test...') - - # Language slide - driver.wait_for_ajax(True) - time.sleep(1) - - driver.set_select("language", "English (United States)") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - - # Region slide - driver.wait_for_ajax(True) - driver.set_select("country", "India") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - - # Profile slide - driver.set_field("full_name", "Great Tester") - driver.set_field("email", "great@example.com") - driver.set_field("password", "test") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - time.sleep(1) - - # domain slide - driver.set_multicheck("domains", ["Manufacturing"]) - time.sleep(1) - driver.click(".next-btn") - - # Org slide - driver.set_field("company_name", "For Testing") - time.sleep(1) - driver.print_console() - driver.click(".next-btn") - - driver.set_field("company_tagline", "Just for GST") - driver.set_field("bank_account", "HDFC") - time.sleep(3) - driver.click(".complete-btn") - - # Wait for desktop - driver.wait_for('#page-desktop', timeout=600) - - driver.print_console() - time.sleep(3) - - frappe.db.set_default('in_selenium', None) - frappe.db.set_value("Company", "For Testing", "write_off_account", "Write Off - FT") - frappe.db.set_value("Company", "For Testing", "exchange_gain_loss_account", "Exchange Gain/Loss - FT") - frappe.db.commit() - - driver.close() - - return True From ad66677029bccd534639ef8158dcf7f1af03e2d7 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 29 Nov 2019 23:59:02 +0530 Subject: [PATCH 13/76] fix: add conditional message in template string --- erpnext/public/js/hub/pages/Search.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/hub/pages/Search.vue b/erpnext/public/js/hub/pages/Search.vue index 2a6088925fd..8ab48d0e8d7 100644 --- a/erpnext/public/js/hub/pages/Search.vue +++ b/erpnext/public/js/hub/pages/Search.vue @@ -42,7 +42,7 @@ export default { computed: { page_title() { return this.items.length - ? __(`Results for ${this.search_value} in category ${this.category}`) + ? __(`Results for "${this.search_value}" ${this.category!=='All'? `in category ${this.category}`: ''}`) : __('No Items found.'); } }, From 4405a2a5f86ff66099a676ec050780b9b8aaf613 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sat, 30 Nov 2019 16:58:31 +0530 Subject: [PATCH 14/76] fix: add project in child for items --- erpnext/projects/doctype/project/project.js | 50 ++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 25c97d1fb84..069e3dc6fee 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -16,6 +16,54 @@ frappe.ui.form.on("Project", { time_log.parenttype = 'Timesheet'; new_doc.time_logs = [time_log]; + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); + }, + 'Purchase Order': () => { + let doctype = 'Purchase Order'; + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + // add a new row and set the project + let purchase_order_item = frappe.model.get_new_doc('Purchase Order Item'); + purchase_order_item.project = frm.doc.name; + purchase_order_item.parent = new_doc.name; + purchase_order_item.parentfield = 'items'; + purchase_order_item.parenttype = 'Purchase Order'; + new_doc.items = [purchase_order_item]; + + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); + }, + 'Purchase Receipt': () => { + let doctype = 'Purchase Receipt'; + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + // add a new row and set the project + let purchase_receipt_item = frappe.model.get_new_doc('Purchase Receipt Item'); + purchase_receipt_item.project = frm.doc.name; + purchase_receipt_item.parent = new_doc.name; + purchase_receipt_item.parentfield = 'items'; + purchase_receipt_item.parenttype = 'Purchase Receipt'; + new_doc.items = [purchase_receipt_item]; + + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); + }, + 'Purchase Invoice': () => { + let doctype = 'Purchase Invoice'; + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + // add a new row and set the project + let purchase_invoice_item = frappe.model.get_new_doc('Purchase Invoice Item'); + purchase_invoice_item.project = frm.doc.name; + purchase_invoice_item.parent = new_doc.name; + purchase_invoice_item.parentfield = 'items'; + purchase_invoice_item.parenttype = 'Purchase Invoice'; + new_doc.items = [purchase_invoice_item]; + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); }, @@ -80,7 +128,7 @@ frappe.ui.form.on("Project", { frm.events.set_status(frm, 'Cancelled'); }, __('Set Status')); } - + if (frappe.model.can_read("Task")) { frm.add_custom_button(__("Gantt Chart"), function () { frappe.route_options = { From 98a54355ae5c88fdfcec488a5c12eb58f52b721a Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 1 Dec 2019 10:06:16 +0530 Subject: [PATCH 15/76] fix: Post GL entry fix for asset (#19751) --- erpnext/assets/doctype/asset/asset.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 40f1e1efc9b..d32f834f0f4 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -517,15 +517,18 @@ def update_maintenance_status(): asset.set_status('Out of Order') def make_post_gl_entry(): - if not is_cwip_accounting_enabled(self.asset_category): - return - assets = frappe.db.sql_list(""" select name from `tabAsset` - where ifnull(booked_fixed_asset, 0) = 0 and available_for_use_date = %s""", nowdate()) + asset_categories = frappe.db.get_all('Asset Category', fields = ['name', 'enable_cwip_accounting']) - for asset in assets: - doc = frappe.get_doc('Asset', asset) - doc.make_gl_entries() + for asset_category in asset_categories: + if cint(asset_category.enable_cwip_accounting): + assets = frappe.db.sql_list(""" select name from `tabAsset` + where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0 + and available_for_use_date = %s""", (asset_category.name, nowdate())) + + for asset in assets: + doc = frappe.get_doc('Asset', asset) + doc.make_gl_entries() def get_asset_naming_series(): meta = frappe.get_meta('Asset') From 5249a6348fbbc861652b9a70c23dedd8502f1c00 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Sun, 1 Dec 2019 06:17:21 +0100 Subject: [PATCH 16/76] Update de.csv (#19755) Changed all translations from "Aufgabe" to "Vorgang" in singular and plural where the word "Task" is project related. Left all agricultural and others how they are (as Aufgabe). --- erpnext/translations/de.csv | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 2e25a127d8e..cdff3ff422b 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1329,7 +1329,7 @@ apps/erpnext/erpnext/hr/doctype/job_offer/job_offer.js,Create Employee,Mitarbeit apps/erpnext/erpnext/utilities/transaction_base.py,Invalid Posting Time,Ungültige Buchungszeit DocType: Salary Component,Condition and Formula,Zustand und Formel DocType: Lead,Campaign Name,Kampagnenname -apps/erpnext/erpnext/setup/default_energy_point_rules.py,On Task Completion,Bei Abschluss der Aufgabe +apps/erpnext/erpnext/setup/default_energy_point_rules.py,On Task Completion,Bei Abschluss des Vorgangs apps/erpnext/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py,There is no leave period in between {0} and {1},Es gibt keinen Urlaub zwischen {0} und {1} DocType: Fee Validity,Healthcare Practitioner,praktischer Arzt DocType: Hotel Room,Capacity,Kapazität @@ -1353,7 +1353,7 @@ DocType: Payment Entry,Received Amount (Company Currency),Erhaltene Menge (Gesel apps/erpnext/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py,Payment Cancelled. Please check your GoCardless Account for more details,Zahlung abgebrochen. Bitte überprüfen Sie Ihr GoCardless Konto für weitere Details DocType: Work Order,Skip Material Transfer to WIP Warehouse,Überspringen Sie die Materialübertragung in das WIP-Lager DocType: Contract,N/A,nicht verfügbar -DocType: Task Type,Task Type,Aufgabentyp +DocType: Task Type,Task Type,Vorgangstyp DocType: Topic,Topic Content,Themeninhalt DocType: Delivery Settings,Send with Attachment,Senden mit Anhang DocType: Service Level,Priorities,Prioritäten @@ -2449,7 +2449,7 @@ apps/erpnext/erpnext/public/js/utils/serial_no_batch_selector.js,Please select b DocType: Asset,Depreciation Schedules,Abschreibungen Termine apps/erpnext/erpnext/projects/doctype/timesheet/timesheet.js,Create Sales Invoice,Verkaufsrechnung erstellen apps/erpnext/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html,Ineligible ITC,Nicht förderfähiges ITC -DocType: Task,Dependent Tasks,Abhängige Aufgaben +DocType: Task,Dependent Tasks,Abhängige Vorgänge apps/erpnext/erpnext/regional/report/gstr_1/gstr_1.py,Following accounts might be selected in GST Settings:,In den GST-Einstellungen können folgende Konten ausgewählt werden: apps/erpnext/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js,Quantity to Produce,Menge zu produzieren apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py,Application period cannot be outside leave allocation period,Beantragter Zeitraum kann nicht außerhalb der beantragten Urlaubszeit liegen @@ -2846,7 +2846,7 @@ DocType: Loan,Applicant Type,Bewerbertyp DocType: Purchase Invoice,03-Deficiency in services,03-Mangel an Dienstleistungen DocType: Healthcare Settings,Default Medical Code Standard,Default Medical Code Standard DocType: Purchase Invoice Item,HSN/SAC,HSN / SAC -DocType: Project Template Task,Project Template Task,Projektvorlagenaufgabe +DocType: Project Template Task,Project Template Task,Projektvorgangsvorlage DocType: Accounts Settings,Over Billing Allowance (%),Mehr als Abrechnungsbetrag (%) apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py,Purchase Receipt {0} is not submitted,Kaufbeleg {0} wurde nicht übertragen DocType: Company,Default Payable Account,Standard-Verbindlichkeitenkonto @@ -3323,7 +3323,7 @@ DocType: Soil Texture,Silt,Schlick ,Qty to Order,Zu bestellende Menge DocType: Period Closing Voucher,"The account head under Liability or Equity, in which Profit/Loss will be booked","Der Kontenkopf unter Eigen- oder Fremdkapital, in dem Gewinn / Verlust verbucht wird" apps/erpnext/erpnext/accounts/doctype/budget/budget.py,Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4},Ein weiterer Budgeteintrag '{0}' existiert bereits für {1} '{2}' und für '{3}' für das Geschäftsjahr {4} -apps/erpnext/erpnext/config/projects.py,Gantt chart of all tasks.,Gantt-Diagramm aller Aufgaben +apps/erpnext/erpnext/config/projects.py,Gantt chart of all tasks.,Gantt-Diagramm aller Vorgänge DocType: Opportunity,Mins to First Response,Minuten zum First Response DocType: Pricing Rule,Margin Type,Margenart apps/erpnext/erpnext/projects/doctype/project/project_dashboard.html,{0} hours,{0} Stunden @@ -3961,7 +3961,7 @@ apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Maintenance Sche apps/erpnext/erpnext/education/doctype/student/student_dashboard.py,Student LMS Activity,Student LMS Aktivität DocType: POS Profile,Applicable for Users,Anwendbar für Benutzer DocType: Supplier Quotation,PUR-SQTN-.YYYY.-,PUR-SQTN-.JJJJ.- -apps/erpnext/erpnext/projects/doctype/project/project.js,Set Project and all Tasks to status {0}?,Projekt und alle Aufgaben auf Status {0} setzen? +apps/erpnext/erpnext/projects/doctype/project/project.js,Set Project and all Tasks to status {0}?,Projekt und alle Vorgänge auf Status {0} setzen? DocType: Purchase Invoice,Set Advances and Allocate (FIFO),Vorschüsse setzen und zuordnen (FIFO) apps/erpnext/erpnext/manufacturing/doctype/production_plan/production_plan.py,No Work Orders created,Keine Arbeitsaufträge erstellt apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py,Salary Slip of employee {0} already created for this period,Gehaltsabrechnung der Mitarbeiter {0} für diesen Zeitraum bereits erstellt @@ -4418,7 +4418,7 @@ DocType: Normal Test Items,Result Value,Ergebnis Wert DocType: Hotel Room,Hotels,Hotels apps/erpnext/erpnext/accounts/doctype/cost_center/cost_center_tree.js,New Cost Center Name,Neuer Kostenstellenname DocType: Leave Control Panel,Leave Control Panel,Urlaubsverwaltung -DocType: Project,Task Completion,Aufgabenerledigung +DocType: Project,Task Completion,Vorgangserfüllung apps/erpnext/erpnext/templates/generators/item/item_add_to_cart.html,Not in Stock,Nicht lagernd DocType: Volunteer,Volunteer Skills,Freiwillige Fähigkeiten DocType: Additional Salary,HR User,Nutzer Personalabteilung @@ -5197,7 +5197,7 @@ DocType: Work Order,Material Transferred for Manufacturing,Material zur Herstell apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py,Account {0} does not exists,Konto {0} existiert nicht apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.js,Select Loyalty Program,Wählen Sie Treueprogramm DocType: Project,Project Type,Projekttyp -apps/erpnext/erpnext/projects/doctype/task/task.py,Child Task exists for this Task. You can not delete this Task.,Für diese Aufgabe existiert eine untergeordnete Aufgabe. Sie können diese Aufgabe daher nicht löschen. +apps/erpnext/erpnext/projects/doctype/task/task.py,Child Task exists for this Task. You can not delete this Task.,Für diesen Vorgang existiert ein untergeordneter Vorgang. Sie können diese Aufgabe daher nicht löschen. apps/erpnext/erpnext/setup/doctype/sales_person/sales_person.py,Either target qty or target amount is mandatory.,Entweder Zielstückzahl oder Zielmenge ist zwingend erforderlich. apps/erpnext/erpnext/config/projects.py,Cost of various activities,Aufwendungen für verschiedene Tätigkeiten apps/erpnext/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py,"Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}","Einstellen Events auf {0}, da die Mitarbeiter auf die beigefügten unter Verkaufs Personen keine Benutzer-ID {1}" @@ -5597,7 +5597,7 @@ apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py,Paid apps/erpnext/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py,{0} is not a valid Batch Number for Item {1},{0} ist keine gültige Chargennummer für Artikel {1} apps/erpnext/erpnext/shopping_cart/cart.py,Please enter valid coupon code !!,Bitte geben Sie einen gültigen Gutscheincode ein !! apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py,Note: There is not enough leave balance for Leave Type {0},Hinweis: Es gibt nicht genügend Urlaubsguthaben für Abwesenheitstyp {0} -DocType: Task,Task Description,Aufgabenbeschreibung +DocType: Task,Task Description,Vorgangsbeschreibung DocType: Training Event,Seminar,Seminar DocType: Program Enrollment Fee,Program Enrollment Fee,Programm Einschreibegebühr DocType: Item,Supplier Items,Lieferantenartikel @@ -5754,7 +5754,7 @@ apps/erpnext/erpnext/accounts/doctype/sales_invoice/pos.py,All Territories,Alle DocType: Lost Reason Detail,Lost Reason Detail,Verlorene Begründung Detail apps/erpnext/erpnext/hr/utils.py,Please set leave policy for employee {0} in Employee / Grade record,Legen Sie die Abwesenheitsrichtlinie für den Mitarbeiter {0} im Mitarbeiter- / Notensatz fest apps/erpnext/erpnext/public/js/controllers/transaction.js,Invalid Blanket Order for the selected Customer and Item,Ungültiger Blankoauftrag für den ausgewählten Kunden und Artikel -apps/erpnext/erpnext/projects/doctype/task/task_tree.js,Add Multiple Tasks,Mehrere Aufgaben hinzufügen +apps/erpnext/erpnext/projects/doctype/task/task_tree.js,Add Multiple Tasks,Mehrere Vorgänge hinzufügen DocType: Purchase Invoice,Items,Artikel apps/erpnext/erpnext/crm/doctype/contract/contract.py,End Date cannot be before Start Date.,Das Enddatum darf nicht vor dem Startdatum liegen. apps/erpnext/erpnext/education/doctype/course_enrollment/course_enrollment.py,Student is already enrolled.,Student ist bereits eingetragen sind. From f89b1722028684f6d1e19bc367de1f547aeea8c3 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sun, 1 Dec 2019 21:53:32 +0530 Subject: [PATCH 17/76] fix: Available stock for packing item report --- .../available_stock_for_packing_items.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py index 32711b2fce0..056492a3274 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py @@ -7,7 +7,7 @@ from frappe.utils import flt def execute(filters=None): if not filters: filters = {} - + columns = get_columns() iwq_map = get_item_warehouse_quantity_map() item_map = get_item_details() @@ -15,22 +15,23 @@ def execute(filters=None): for sbom, warehouse in iwq_map.items(): total = 0 total_qty = 0 - + for wh, item_qty in warehouse.items(): total += 1 - row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, - item_map.get(sbom).stock_uom, wh] - available_qty = item_qty - total_qty += flt(available_qty) - row += [available_qty] - - if available_qty: - data.append(row) - if (total == len(warehouse)): - row = ["", "", "Total", "", "", total_qty] + if item_map.get(sbom): + row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, + item_map.get(sbom).stock_uom, wh] + available_qty = item_qty + total_qty += flt(available_qty) + row += [available_qty] + + if available_qty: data.append(row) + if (total == len(warehouse)): + row = ["", "", "Total", "", "", total_qty] + data.append(row) return columns, data - + def get_columns(): columns = ["Item Code:Link/Item:100", "Item Name::100", "Description::120", \ "UOM:Link/UOM:80", "Warehouse:Link/Warehouse:100", "Quantity::100"] From 957a6f5bd5e7781d33da28a5b6c0f0e54a1b0235 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Mon, 2 Dec 2019 14:44:17 +0530 Subject: [PATCH 18/76] fix: UX/UI (#19753) --- erpnext/hr/doctype/payroll_entry/payroll_entry.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.js b/erpnext/hr/doctype/payroll_entry/payroll_entry.js index adc06712ef0..d25eb6d7810 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.js @@ -31,7 +31,11 @@ frappe.ui.form.on('Payroll Entry', { } if ((frm.doc.employees || []).length) { frm.page.set_primary_action(__('Create Salary Slips'), () => { - frm.save('Submit'); + frm.save('Submit').then(()=>{ + frm.page.clear_primary_action(); + frm.refresh(); + frm.events.refresh(frm); + }); }); } } From 2fa204d8be98f24136f2cf3eb4c2408f86f35b9f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 2 Dec 2019 16:26:10 +0530 Subject: [PATCH 19/76] fix: render_template for subject in Email Campaign (#19772) --- erpnext/crm/doctype/email_campaign/email_campaign.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 3050d05a7c8..00a4bd1a322 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -41,7 +41,8 @@ class EmailCampaign(Document): email_campaign_exists = frappe.db.exists("Email Campaign", { "campaign_name": self.campaign_name, "recipient": self.recipient, - "status": ("in", ["In Progress", "Scheduled"]) + "status": ("in", ["In Progress", "Scheduled"]), + "name": ("!=", self.name) }) if email_campaign_exists: frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) @@ -78,7 +79,7 @@ def send_mail(entry, email_campaign): comm = make( doctype = "Email Campaign", name = email_campaign.name, - subject = email_template.get("subject"), + subject = frappe.render_template(email_template.get("subject"), context), content = frappe.render_template(email_template.get("response"), context), sender = sender, recipients = recipient, From de5237fbd50748f1c9e019a677cc800d5d1b321c Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Mon, 2 Dec 2019 17:08:12 +0530 Subject: [PATCH 20/76] refactor: Report BOM Sock Calculated (#19431) --- .../bom_stock_calculated.py | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index be016ad9a47..f7b407b7922 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils.data import comma_and def execute(filters=None): # if not filters: filters = {} @@ -13,35 +14,36 @@ def execute(filters=None): data = get_bom_stock(filters) qty_to_make = filters.get("qty_to_make") + manufacture_details = get_manufacturer_records() for row in data: - item_map = get_item_details(row.item_code) reqd_qty = qty_to_make * row.actual_qty last_pur_price = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") - if row.to_build > 0: - diff_qty = row.to_build - reqd_qty - summ_data.append([row.item_code, row.description, item_map[row.item_code]["manufacturer"], item_map[row.item_code]["manufacturer_part_no"], row.actual_qty, row.to_build, reqd_qty, diff_qty, last_pur_price]) - else: - diff_qty = 0 - reqd_qty - summ_data.append([row.item_code, row.description, item_map[row.item_code]["manufacturer"], item_map[row.item_code]["manufacturer_part_no"], row.actual_qty, "0.000", reqd_qty, diff_qty, last_pur_price]) + summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details)) return columns, summ_data +def get_report_data(last_pur_price, reqd_qty, row, manufacture_details): + to_build = row.to_build if row.to_build > 0 else 0 + diff_qty = to_build - reqd_qty + return [row.item_code, row.description, + comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer', []), add_quotes=False), + comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer_part', []), add_quotes=False), + row.actual_qty, str(to_build), + reqd_qty, diff_qty, last_pur_price] + def get_columns(): """return columns""" columns = [ _("Item") + ":Link/Item:100", _("Description") + "::150", - _("Manufacturer") + "::100", - _("Manufacturer Part Number") + "::100", + _("Manufacturer") + "::250", + _("Manufacturer Part Number") + "::250", _("Qty") + ":Float:50", _("Stock Qty") + ":Float:100", _("Reqd Qty")+ ":Float:100", _("Diff Qty")+ ":Float:100", _("Last Purchase Price")+ ":Float:100", - - ] - return columns def get_bom_stock(filters): @@ -85,7 +87,12 @@ def get_bom_stock(filters): GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) -def get_item_details(item_code): - items = frappe.db.sql("""select it.item_group, it.item_name, it.stock_uom, it.name, it.brand, it.description, it.manufacturer_part_no, it.manufacturer from tabItem it where it.item_code = %s""", item_code, as_dict=1) +def get_manufacturer_records(): + details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no, parent"]) + manufacture_details = frappe._dict() + for detail in details: + dic = manufacture_details.setdefault(detail.get('parent'), {}) + dic.setdefault('manufacturer', []).append(detail.get('manufacturer')) + dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no')) - return dict((d.name, d) for d in items) + return manufacture_details \ No newline at end of file From a0f9db4b8315ad88e8c91965d1e45dbfcd0942e3 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 3 Dec 2019 12:51:27 +0530 Subject: [PATCH 21/76] Update employee.py --- erpnext/hr/doctype/employee/employee.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 703ec06f83b..2f88e1e3631 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime +from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ @@ -218,8 +218,8 @@ class Employee(NestedSet): def reset_employee_emails_cache(self): prev_doc = self.get_doc_before_save() or {} - cell_number = self.get('cell_number') - prev_number = prev_doc.get('cell_number') + cell_number = cstr(self.get('cell_number')) + prev_number = cstr(prev_doc.get('cell_number')) if (cell_number != prev_number or self.get('user_id') != prev_doc.get('user_id')): frappe.cache().hdel('employees_with_number', cell_number) From 728f8d09c89b146204cb3a155a8b3be191440b0d Mon Sep 17 00:00:00 2001 From: gavin Date: Tue, 3 Dec 2019 12:54:46 +0530 Subject: [PATCH 22/76] fix: AttributeError on new Student creation (#19786) --- erpnext/education/doctype/student/student.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 9af5e22913a..76825cec1b2 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -40,7 +40,7 @@ class Student(Document): frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant)) def after_insert(self): - if not frappe.get_single('Education Settings').user_creation_skip: + if not frappe.get_single('Education Settings').get('user_creation_skip'): self.create_student_user() def create_student_user(self): From c68a940aac30e21347731894ed26ffaf1355e513 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 3 Dec 2019 12:59:22 +0530 Subject: [PATCH 23/76] fix: Item qty cannot be zero in Purchase Receipt (#19779) --- erpnext/controllers/accounts_controller.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1f8b6635958..4e0dd6f1e61 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -415,9 +415,10 @@ class AccountsController(TransactionBase): return gl_dict def validate_qty_is_not_zero(self): - for item in self.items: - if not item.qty: - frappe.throw(_("Item quantity can not be zero")) + if self.doctype != "Purchase Receipt": + for item in self.items: + if not item.qty: + frappe.throw(_("Item quantity can not be zero")) def validate_account_currency(self, account, account_currency=None): valid_currency = [self.company_currency] From 03d1394540b11b7cc7f242c662cbd0d117a883dc Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 3 Dec 2019 15:12:44 +0530 Subject: [PATCH 24/76] fix: Unable to see parties with negative balance in AR/AP Summary (#19776) --- .../accounts_receivable_summary/accounts_receivable_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 8955830e09b..b607c0f7028 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -36,7 +36,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.filters.report_date) or {} for party, party_dict in iteritems(self.party_total): - if party_dict.outstanding <= 0: + if party_dict.outstanding == 0: continue row = frappe._dict() From 8d31171ff8903dc56ca833dc783e8eb25dc4091a Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 3 Dec 2019 16:31:07 +0530 Subject: [PATCH 25/76] fix: Party name field in trial balacne for party report (#19789) --- .../trial_balance_for_party.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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 bd2c34b3b4c..3e47906a98b 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 @@ -18,14 +18,17 @@ def execute(filters=None): return columns, data def get_data(filters, show_party_name): - party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) + if filters.get('party_type') in ('Customer', 'Supplier', 'Employee', 'Member'): + 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' + else: + party_name_field = 'name' 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], filters = party_filters, order_by="name") company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") opening_balances = get_opening_balances(filters) @@ -70,7 +73,7 @@ def get_data(filters, show_party_name): # totals for col in total_row: total_row[col] += row.get(col) - + row.update({ "currency": company_currency }) @@ -78,7 +81,7 @@ def get_data(filters, show_party_name): has_value = False if (opening_debit or opening_credit or debit or credit or closing_debit or closing_credit): has_value =True - + if cint(filters.show_zero_values) or has_value: data.append(row) @@ -94,9 +97,9 @@ def get_data(filters, show_party_name): def get_opening_balances(filters): gle = frappe.db.sql(""" - select party, sum(debit) as opening_debit, sum(credit) as opening_credit + select party, sum(debit) as opening_debit, sum(credit) as opening_credit from `tabGL Entry` - where company=%(company)s + where company=%(company)s and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') group by party""", { @@ -114,11 +117,11 @@ def get_opening_balances(filters): def get_balances_within_period(filters): gle = frappe.db.sql(""" - select party, sum(debit) as debit, sum(credit) as credit + select party, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` - where company=%(company)s + where company=%(company)s and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and posting_date >= %(from_date)s and posting_date <= %(to_date)s + and posting_date >= %(from_date)s and posting_date <= %(to_date)s and ifnull(is_opening, 'No') = 'No' group by party""", { "company": filters.company, From e176c3c1d1dc138f5155a979002e7ef92a7daa67 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 3 Dec 2019 16:56:48 +0530 Subject: [PATCH 26/76] feat: Receivable / payable summary based on payment terms --- .../accounts_payable_summary/accounts_payable_summary.js | 5 +++++ .../accounts_receivable_summary.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 5f0fdc9f2c7..4a9f1b0dc44 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -88,6 +88,11 @@ frappe.query_reports["Accounts Payable Summary"] = { "label": __("Supplier Group"), "fieldtype": "Link", "options": "Supplier Group" + }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", } ], diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 0120608a8ff..d54824b6855 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -106,6 +106,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { "label": __("Sales Person"), "fieldtype": "Link", "options": "Sales Person" + }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", } ], From 73129e6ac8e5931dab2ecc7d788d160c163fddb5 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Tue, 3 Dec 2019 17:03:45 +0530 Subject: [PATCH 27/76] fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' (#19774) * fix: date validation on inpatient record, else condition removing on clinical prcd templ which is not req * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 430dce7ddbb..e871d98af6a 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -34,8 +34,7 @@ class PricingRule(Document): def validate_duplicate_apply_on(self): field = apply_on_dict.get(self.apply_on) - values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field)] - + values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field] if len(values) != len(set(values)): frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on)) From d635bda29ba9a5ab3b4e4e33594f9c29f929aab6 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 3 Dec 2019 17:05:06 +0530 Subject: [PATCH 28/76] fix selling setting label for sales order reqd (#19775) --- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../selling_settings/selling_settings.json | 722 ++++-------------- .../selling_settings/test_selling_settings.py | 10 + 3 files changed, 146 insertions(+), 588 deletions(-) create mode 100644 erpnext/selling/doctype/selling_settings/test_selling_settings.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index def671c19b7..d024a31162f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -538,7 +538,7 @@ class SalesInvoice(SellingController): is_stock_item = frappe.get_cached_value('Item', d.item_code, 'is_stock_item') if (d.item_code and is_stock_item == 1\ and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])): - msgprint(_("{0} is mandatory for Item {1}").format(i,d.item_code), raise_exception=1) + msgprint(_("{0} is mandatory for Stock Item {1}").format(i,d.item_code), raise_exception=1) def validate_proj_cust(self): diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index dc2c4ce4a96..5033d7aa7f9 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -1,611 +1,159 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-06-25 10:25:16", - "custom": 0, - "description": "Settings for Selling Module", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Other", - "editable_grid": 0, - "engine": "InnoDB", + "creation": "2013-06-25 10:25:16", + "description": "Settings for Selling Module", + "doctype": "DocType", + "document_type": "Other", + "engine": "InnoDB", + "field_order": [ + "cust_master_name", + "campaign_naming_by", + "customer_group", + "territory", + "selling_price_list", + "close_opportunity_after_days", + "default_valid_till", + "column_break_5", + "so_required", + "dn_required", + "sales_update_frequency", + "maintain_same_sales_rate", + "editable_price_list_rate", + "allow_multiple_items", + "allow_against_multiple_purchase_orders", + "validate_selling_price", + "hide_tax_id" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Customer Name", - "fieldname": "cust_master_name", - "fieldtype": "Select", - "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": "Customer Naming By", - "length": 0, - "no_copy": 0, - "options": "Customer Name\nNaming Series", - "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 - }, + "default": "Customer Name", + "fieldname": "cust_master_name", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Customer Naming By", + "options": "Customer Name\nNaming Series" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "campaign_naming_by", - "fieldtype": "Select", - "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": "Campaign Naming By", - "length": 0, - "no_copy": 0, - "options": "Campaign Name\nNaming Series", - "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 - }, + "fieldname": "campaign_naming_by", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Campaign Naming By", + "options": "Campaign Name\nNaming Series" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "customer_group", - "fieldtype": "Link", - "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": "Default Customer Group", - "length": 0, - "no_copy": 0, - "options": "Customer Group", - "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 - }, + "fieldname": "customer_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Customer Group", + "options": "Customer Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "territory", - "fieldtype": "Link", - "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": "Default Territory", - "length": 0, - "no_copy": 0, - "options": "Territory", - "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 - }, + "fieldname": "territory", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Territory", + "options": "Territory" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "selling_price_list", - "fieldtype": "Link", - "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": "Default Price List", - "length": 0, - "no_copy": 0, - "options": "Price List", - "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 - }, + "fieldname": "selling_price_list", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Price List", + "options": "Price List" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "15", - "description": "Auto close Opportunity after 15 days", - "fieldname": "close_opportunity_after_days", - "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": "Close Opportunity After Days", - "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 - }, + "default": "15", + "description": "Auto close Opportunity after 15 days", + "fieldname": "close_opportunity_after_days", + "fieldtype": "Int", + "label": "Close Opportunity After Days" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_valid_till", - "fieldtype": "Data", - "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 Quotation Validity Days", - "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 - }, + "fieldname": "default_valid_till", + "fieldtype": "Data", + "label": "Default Quotation Validity Days" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "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, - "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 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "so_required", - "fieldtype": "Select", - "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": "Sales Order Required", - "length": 0, - "no_copy": 0, - "options": "No\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 - }, + "description": "Only for Stock Items", + "fieldname": "so_required", + "fieldtype": "Select", + "label": "Sales Order Required", + "options": "No\nYes" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dn_required", - "fieldtype": "Select", - "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 Note Required", - "length": 0, - "no_copy": 0, - "options": "No\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 - }, + "fieldname": "dn_required", + "fieldtype": "Select", + "label": "Delivery Note Required", + "options": "No\nYes" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Each Transaction", - "description": "How often should project and company be updated based on Sales Transactions.", - "fieldname": "sales_update_frequency", - "fieldtype": "Select", - "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": "Sales Update Frequency", - "length": 0, - "no_copy": 0, - "options": "Each Transaction\nDaily\nMonthly", - "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 - }, + "default": "Each Transaction", + "description": "How often should project and company be updated based on Sales Transactions.", + "fieldname": "sales_update_frequency", + "fieldtype": "Select", + "label": "Sales Update Frequency", + "options": "Each Transaction\nDaily\nMonthly", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintain_same_sales_rate", - "fieldtype": "Check", - "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": "Maintain Same Rate Throughout Sales Cycle", - "length": 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 - }, + "default": "0", + "fieldname": "maintain_same_sales_rate", + "fieldtype": "Check", + "label": "Maintain Same Rate Throughout Sales Cycle" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "editable_price_list_rate", - "fieldtype": "Check", - "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": "Allow user to edit Price List Rate in transactions", - "length": 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 - }, + "default": "0", + "fieldname": "editable_price_list_rate", + "fieldtype": "Check", + "label": "Allow user to edit Price List Rate in transactions" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_multiple_items", - "fieldtype": "Check", - "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": "Allow Item to be added multiple times in a transaction", - "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 - }, + "default": "0", + "fieldname": "allow_multiple_items", + "fieldtype": "Check", + "label": "Allow Item to be added multiple times in a transaction" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_against_multiple_purchase_orders", - "fieldtype": "Check", - "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": "Allow multiple Sales Orders against a Customer's Purchase Order", - "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 - }, + "default": "0", + "fieldname": "allow_against_multiple_purchase_orders", + "fieldtype": "Check", + "label": "Allow multiple Sales Orders against a Customer's Purchase Order" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "validate_selling_price", - "fieldtype": "Check", - "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": "Validate Selling Price for Item against Purchase Rate or Valuation Rate", - "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 - }, + "default": "0", + "fieldname": "validate_selling_price", + "fieldtype": "Check", + "label": "Validate Selling Price for Item against Purchase Rate or Valuation Rate" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hide_tax_id", - "fieldtype": "Check", - "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": "Hide Customer's Tax Id from Sales Transactions", - "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 + "default": "0", + "fieldname": "hide_tax_id", + "fieldtype": "Check", + "label": "Hide Customer's Tax Id from Sales Transactions" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-cog", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2018-06-25 12:56:16.332039", - "modified_by": "Administrator", - "module": "Selling", - "name": "Selling Settings", - "owner": "Administrator", + ], + "icon": "fa fa-cog", + "idx": 1, + "issingle": 1, + "modified": "2019-11-25 18:35:51.472653", + "modified_by": "Administrator", + "module": "Selling", + "name": "Selling Settings", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/test_selling_settings.py b/erpnext/selling/doctype/selling_settings/test_selling_settings.py new file mode 100644 index 00000000000..961a54de0a8 --- /dev/null +++ b/erpnext/selling/doctype/selling_settings/test_selling_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSellingSettings(unittest.TestCase): + pass From 5128f3bd0fcd82283e8d59f40f27a150d2505486 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 3 Dec 2019 17:27:22 +0530 Subject: [PATCH 29/76] feat: allow searching from meta fields (#19711) * feat: allow searching from meta fields * feat: remove description in query based on number of items --- erpnext/portal/product_configurator/utils.py | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 61c50e5fe01..3a373a4ab19 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -313,13 +313,25 @@ def get_items(filters=None, search=None): search_condition = '' if search: + # Default fields to search from + default_fields = {'name', 'item_name', 'description', 'item_group'} + + # Get meta search fields + meta = frappe.get_meta("Item") + meta_fields = set(meta.get_search_fields()) + + # Join the meta fields and default fields set + search_fields = default_fields.union(meta_fields) + try: + if frappe.db.count('Item', cache=True) > 50000: + search_fields.remove('description') + except KeyError: + pass + + # Build or filters for query search = '%{}%'.format(search) - or_filters = [ - ['name', 'like', search], - ['item_name', 'like', search], - ['description', 'like', search], - ['item_group', 'like', search] - ] + or_filters = [[field, 'like', search] for field in search_fields] + search_condition = get_conditions(or_filters, 'or') filter_condition = get_conditions(filters, 'and') From be88d0f8a359c7e2ff8b40e5a3eb34014ff497dd Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 3 Dec 2019 18:13:39 +0530 Subject: [PATCH 30/76] fix: Custom button conditionally visible in draft condition as well --- .../stock/doctype/stock_entry/stock_entry.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 96e74ccca1a..5c8efcbc01e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -238,6 +238,8 @@ frappe.ui.form.on('Stock Entry', { }, __("Get items from")); } + frm.events.show_bom_custom_button(frm); + if (frm.doc.company) { frm.trigger("toggle_display_account_head"); } @@ -253,13 +255,7 @@ frappe.ui.form.on('Stock Entry', { stock_entry_type: function(frm){ frm.remove_custom_button('Bill of Materials', "Get items from"); - - if (frm.doc.docstatus === 0 && - ['Material Issue', 'Material Receipt', 'Material Transfer', 'Send to Subcontractor'].includes(frm.doc.purpose)) { - frm.add_custom_button(__('Bill of Materials'), function() { - frm.events.get_items_from_bom(frm); - }, __("Get items from")); - } + frm.events.show_bom_custom_button(frm); }, purpose: function(frm) { @@ -398,6 +394,15 @@ frappe.ui.form.on('Stock Entry', { } }, + show_bom_custom_button: function(frm){ + if (frm.doc.docstatus === 0 && + ['Material Issue', 'Material Receipt', 'Material Transfer', 'Send to Subcontractor'].includes(frm.doc.purpose)) { + frm.add_custom_button(__('Bill of Materials'), function() { + frm.events.get_items_from_bom(frm); + }, __("Get items from")); + } + }, + get_items_from_bom: function(frm) { let filters = function(){ return {filters: { docstatus:1 }}; @@ -417,11 +422,11 @@ frappe.ui.form.on('Stock Entry', { {"fieldname":"fetch", "label":__("Get Items from BOM"), "fieldtype":"Button"} ] - //Exclude field 'Target Warehouse' in case of Material Issue + // Exclude field 'Target Warehouse' in case of Material Issue if (frm.doc.purpose == 'Material Issue'){ fields.splice(2,1); } - //Exclude field 'Source Warehouse' in case of Material Receipt + // Exclude field 'Source Warehouse' in case of Material Receipt else if(frm.doc.purpose == 'Material Receipt'){ fields.splice(1,1); } From a639f16dc9080585645b79f6631c3e43cbdd4d79 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Wed, 4 Dec 2019 09:46:42 +0530 Subject: [PATCH 31/76] fix: column data not visible after manual selection of columns --- erpnext/public/js/financial_statements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 63e057c39d1..dead309a5d0 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -4,7 +4,7 @@ erpnext.financial_statements = { "filters": get_filters(), "formatter": function(value, row, column, data, default_formatter) { if (column.fieldname=="account") { - value = data.account_name; + value = data.account_name || value; column.link_onclick = "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; From 51f2131a41b4c6cb57241aa8b35cc170287efac0 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Wed, 4 Dec 2019 11:56:44 +0530 Subject: [PATCH 32/76] fix: joining and relieving Date can be on same date as valid use case on emoloyee form (#19798) * fix: date validation on inpatient record, else condition removing on clinical prcd templ which is not req * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix: joining and relieving Date can be on same date as valid use case * Update employee.py --- erpnext/hr/doctype/employee/employee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 2f88e1e3631..242531bd177 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -152,8 +152,8 @@ class Employee(NestedSet): elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)): throw(_("Date Of Retirement must be greater than Date of Joining")) - elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) <= getdate(self.date_of_joining)): - throw(_("Relieving Date must be greater than Date of Joining")) + elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) < getdate(self.date_of_joining)): + throw(_("Relieving Date must be greater than or equal to Date of Joining")) elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)): throw(_("Contract End Date must be greater than Date of Joining")) From eb9c3e1c5b68cd348b35b91dcf392e6ebb05480e Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Wed, 4 Dec 2019 12:01:31 +0530 Subject: [PATCH 33/76] fix(patch): set proper tax_type and proper account (#19794) --- .../move_item_tax_to_item_tax_template.py | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index f25b9eaf521..e47344bd92c 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -62,12 +62,12 @@ def execute(): ] for dt in doctypes: - for d in frappe.db.sql("""select name, parent, item_code, item_tax_rate from `tab{0} Item` + for d in frappe.db.sql("""select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item` where ifnull(item_tax_rate, '') not in ('', '{{}}') and item_tax_template is NULL""".format(dt), as_dict=1): item_tax_map = json.loads(d.item_tax_rate) item_tax_template_name = get_item_tax_template(item_tax_templates, - item_tax_map, d.item_code, d.parent) + item_tax_map, d.item_code, d.parenttype, d.parent) frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name) frappe.db.auto_commit_on_many_writes = False @@ -77,7 +77,7 @@ def execute(): settings.determine_address_tax_category_from = "Billing Address" settings.save() -def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=None): +def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None): # search for previously created item tax template by comparing tax maps for template, item_tax_template_map in iteritems(item_tax_templates): if item_tax_map == item_tax_template_map: @@ -88,23 +88,44 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=No item_tax_template.title = make_autoname("Item Tax Template-.####") for tax_type, tax_rate in iteritems(item_tax_map): - if not frappe.db.exists("Account", tax_type): + account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type'], as_dict=1) + if account_details: + if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): + frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable') + else: parts = tax_type.strip().split(" - ") account_name = " - ".join(parts[:-1]) - company = frappe.db.get_value("Company", filters={"abbr": parts[-1]}) + company = get_company(parts[-1], parenttype, parent) parent_account = frappe.db.get_value("Account", filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account") - - frappe.get_doc({ - "doctype": "Account", + filters = { "account_name": account_name, - "company": company, - "account_type": "Tax", - "parent_account": parent_account - }).insert() + "company": company, + "account_type": "Tax", + "parent_account": parent_account + } + tax_type = frappe.db.get_value("Account", filters) + if not tax_type: + account = frappe.new_doc("Account") + account.update(filters) + account.insert() + tax_type = account.name item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate}) item_tax_templates.setdefault(item_tax_template.title, {}) item_tax_templates[item_tax_template.title][tax_type] = tax_rate item_tax_template.save() return item_tax_template.name + +def get_company(company_abbr, parenttype=None, parent=None): + if parenttype and parent: + company = frappe.get_cached_value(parenttype, parent, 'company') + else: + company = frappe.db.get_value("Company", filters={"abbr": company_abbr}) + + if not company: + companies = frappe.get_all('Company') + if len(companies) == 1: + company = companies[0].name + + return company From a830f89a59ab5a1072840eabdfd91763cb22565a Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 4 Dec 2019 13:32:31 +0530 Subject: [PATCH 34/76] feat: add date filter in the fixed asset register (#19799) * feat: add date filter in the fixed asset register * fix: remove function from keyword argument --- .../fixed_asset_register.js | 6 ++++ .../fixed_asset_register.py | 29 ++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 426caaad92d..8c737d066b9 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -26,5 +26,11 @@ frappe.query_reports["Fixed Asset Register"] = { fieldtype: "Link", options: "Finance Book" }, + { + fieldname:"date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today() + }, ] }; diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index f395499ad6b..57b68b4ed24 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cstr +from frappe.utils import cstr, today, flt def execute(filters=None): filters = frappe._dict(filters or {}) @@ -86,8 +86,8 @@ def get_columns(filters): "width": 90 }, { - "label": _("Current Value"), - "fieldname": "current_value", + "label": _("Asset Value"), + "fieldname": "asset_value", "options": "Currency", "width": 90 }, @@ -114,7 +114,7 @@ def get_data(filters): data = [] conditions = get_conditions(filters) - current_value_map = get_finance_book_value_map(filters.finance_book) + depreciation_amount_map = get_finance_book_value_map(filters.date, filters.finance_book) pr_supplier_map = get_purchase_receipt_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map() @@ -125,7 +125,9 @@ def get_data(filters): "available_for_use_date", "status", "purchase_invoice"]) for asset in assets_record: - if current_value_map.get(asset.name) is not None: + asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ + - flt(depreciation_amount_map.get(asset.name)) + if asset_value: row = { "asset_id": asset.name, "asset_name": asset.asset_name, @@ -138,19 +140,24 @@ def get_data(filters): "location": asset.location, "asset_category": asset.asset_category, "purchase_date": asset.purchase_date, - "current_value": current_value_map.get(asset.name) + "asset_value": asset_value } data.append(row) return data -def get_finance_book_value_map(finance_book=''): +def get_finance_book_value_map(date, finance_book=''): + if not date: + date = today() return frappe._dict(frappe.db.sql(''' Select - parent, value_after_depreciation - FROM `tabAsset Finance Book` + parent, SUM(depreciation_amount) + FROM `tabDepreciation Schedule` WHERE - parentfield='finance_books' - AND ifnull(finance_book, '')=%s''', cstr(finance_book))) + parentfield='schedules' + AND schedule_date<=%s + AND journal_entry IS NOT NULL + AND ifnull(finance_book, '')=%s + GROUP BY parent''', (date, cstr(finance_book)))) def get_purchase_receipt_supplier_map(): return frappe._dict(frappe.db.sql(''' Select From acdd5081da0465a436c86bab30449b79a6d8a8d5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 4 Dec 2019 15:30:01 +0530 Subject: [PATCH 35/76] fix: Service start and end date validation for deferred accounting (#19805) --- .../purchase_invoice_item.json | 13 +++++++++---- .../sales_invoice_item/sales_invoice_item.json | 13 +++++++++---- erpnext/controllers/accounts_controller.py | 13 ++++++++++++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 27d8233a44b..acb0398b5c0 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -507,7 +508,8 @@ "depends_on": "enable_deferred_expense", "fieldname": "service_stop_date", "fieldtype": "Date", - "label": "Service Stop Date" + "label": "Service Stop Date", + "no_copy": 1 }, { "default": "0", @@ -523,13 +525,15 @@ "depends_on": "enable_deferred_expense", "fieldname": "service_start_date", "fieldtype": "Date", - "label": "Service Start Date" + "label": "Service Start Date", + "no_copy": 1 }, { "depends_on": "enable_deferred_expense", "fieldname": "service_end_date", "fieldtype": "Date", - "label": "Service End Date" + "label": "Service End Date", + "no_copy": 1 }, { "fieldname": "reference", @@ -766,7 +770,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-21 16:27:52.043744", + "links": [], + "modified": "2019-12-04 12:23:17.046413", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 779ac4f656c..b2294e4318f 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-06-04 11:02:19", "doctype": "DocType", @@ -484,7 +485,8 @@ "depends_on": "enable_deferred_revenue", "fieldname": "service_stop_date", "fieldtype": "Date", - "label": "Service Stop Date" + "label": "Service Stop Date", + "no_copy": 1 }, { "default": "0", @@ -500,13 +502,15 @@ "depends_on": "enable_deferred_revenue", "fieldname": "service_start_date", "fieldtype": "Date", - "label": "Service Start Date" + "label": "Service Start Date", + "no_copy": 1 }, { "depends_on": "enable_deferred_revenue", "fieldname": "service_end_date", "fieldtype": "Date", - "label": "Service End Date" + "label": "Service End Date", + "no_copy": 1 }, { "collapsible": 1, @@ -783,7 +787,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-07-16 16:36:46.527606", + "links": [], + "modified": "2019-12-04 12:22:38.517710", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4e0dd6f1e61..75564afe59e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -61,7 +61,6 @@ class AccountsController(TransactionBase): _('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1) def validate(self): - if not self.get('is_return'): self.validate_qty_is_not_zero() @@ -100,11 +99,23 @@ class AccountsController(TransactionBase): if self.is_return: self.validate_qty() + else: + self.validate_deferred_start_and_end_date() validate_regional(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) + def validate_deferred_start_and_end_date(self): + for d in self.items: + if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): + if not (d.service_start_date and d.service_end_date): + frappe.throw(_("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx)) + elif getdate(d.service_start_date) > getdate(d.service_end_date): + frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) + elif getdate(self.posting_date) > getdate(d.service_end_date): + frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) + def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() self.set_due_date() From d285e9b7bd428c79edaa79bb7ee852d425f9b5c4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 4 Dec 2019 15:30:58 +0530 Subject: [PATCH 36/76] optimize: Optimization of Receivable report filtered based on sales person (#19796) --- .../accounts_receivable.py | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 41989bf863e..2c53f6e9971 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -60,6 +60,7 @@ class ReceivablePayableReport(object): def get_data(self): self.get_gl_entries() + self.get_sales_invoices_or_customers_based_on_sales_person() self.voucher_balance = OrderedDict() self.init_voucher_balance() # invoiced, paid, credit_note, outstanding @@ -103,12 +104,18 @@ class ReceivablePayableReport(object): def get_invoices(self, gle): if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): - self.invoices.add(gle.voucher_no) + if self.filters.get("sales_person"): + if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \ + or gle.party in self.sales_person_records.get("Customer", []): + self.invoices.add(gle.voucher_no) + else: + self.invoices.add(gle.voucher_no) def update_voucher_balance(self, gle): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance row = self.get_voucher_balance(gle) + if not row: return # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) @@ -129,8 +136,13 @@ class ReceivablePayableReport(object): row.paid -= gle_balance def get_voucher_balance(self, gle): - voucher_balance = None + if self.filters.get("sales_person"): + against_voucher = gle.against_voucher or gle.voucher_no + if not (gle.party in self.sales_person_records.get("Customer", []) or \ + against_voucher in self.sales_person_records.get("Sales Invoice", [])): + return + voucher_balance = None if gle.against_voucher: # find invoice against_voucher = gle.against_voucher @@ -512,6 +524,22 @@ class ReceivablePayableReport(object): order by posting_date, party""" .format(select_fields, conditions), values, as_dict=True) + def get_sales_invoices_or_customers_based_on_sales_person(self): + if self.filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", + self.filters.get("sales_person"), ["lft", "rgt"]) + + records = frappe.db.sql(""" + select distinct parent, parenttype + from `tabSales Team` steam + where parenttype in ('Customer', 'Sales Invoice') + and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) + """, (lft, rgt), as_dict=1) + + self.sales_person_records = frappe._dict() + for d in records: + self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent) + def prepare_conditions(self): conditions = [""] values = [self.party_type, self.filters.report_date] @@ -564,16 +592,6 @@ class ReceivablePayableReport(object): conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)") values.append(self.filters.get("sales_partner")) - if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) - - conditions.append("""exists(select name from `tabSales Team` steam where - steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) - and ((steam.parent = voucher_no and steam.parenttype = voucher_type) - or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) - def add_supplier_filters(self, conditions, values): if self.filters.get("supplier_group"): conditions.append("""party in (select name from tabSupplier From 114e0db4190cb12964055fcc2fed76c1d7cf49ad Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Wed, 4 Dec 2019 10:01:43 +0000 Subject: [PATCH 37/76] fix: query for finding lost quotation (#19800) * fix: query for finding lost quotation * Update opportunity.py --- erpnext/crm/doctype/opportunity/opportunity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 99486fa2066..2880c8050e0 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -130,10 +130,10 @@ class Opportunity(TransactionBase): def has_lost_quotation(self): lost_quotation = frappe.db.sql(""" - select q.name - from `tabQuotation` q, `tabQuotation Item` qi - where q.name = qi.parent and q.docstatus=1 - and qi.prevdoc_docname =%s and q.status = 'Lost' + select name + from `tabQuotation` + where docstatus=1 + and opportunity =%s and status = 'Lost' """, self.name) if lost_quotation: if self.has_active_quotation(): From 370cdc017022252bf6a5faaee8abc9f8d7dca23f Mon Sep 17 00:00:00 2001 From: ronelvcabrera <44422325+ronelvcabrera@users.noreply.github.com> Date: Wed, 4 Dec 2019 18:37:11 +0800 Subject: [PATCH 38/76] feat(Sales/Purchase Order): optional to reference a Blanket Order (#19612) --- .../purchase_order/test_purchase_order.py | 26 ++++++++++++++++++- .../purchase_order_item.json | 12 ++++----- .../doctype/blanket_order/blanket_order.py | 4 +++ erpnext/public/js/controllers/transaction.js | 8 ++++++ .../doctype/sales_order/test_sales_order.py | 23 +++++++++++++++- .../sales_order_item/sales_order_item.json | 11 +++++++- erpnext/stock/get_item_details.py | 13 ++++++---- 7 files changed, 83 insertions(+), 14 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index a0a1e8ed5c4..08f5d8b4d00 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -17,6 +17,8 @@ from erpnext.stock.doctype.material_request.material_request import make_purchas from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.status_updater import OverAllowanceError +from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order + class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -620,6 +622,27 @@ class TestPurchaseOrder(unittest.TestCase): po.save() self.assertEqual(po.schedule_date, add_days(nowdate(), 2)) + + def test_po_optional_blanket_order(self): + """ + Expected result: Blanket order Ordered Quantity should only be affected on Purchase Order with against_blanket_order = 1. + Second Purchase Order should not add on to Blanket Orders Ordered Quantity. + """ + + bo = make_blanket_order(blanket_order_type = "Purchasing", quantity = 10, rate = 10) + + po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) + po_doc = frappe.get_doc('Purchase Order', po.get('name')) + # To test if the PO has a Blanket Order + self.assertTrue(po_doc.items[0].blanket_order) + + po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) + po_doc = frappe.get_doc('Purchase Order', po.get('name')) + # To test if the PO does NOT have a Blanket Order + self.assertEqual(po_doc.items[0].blanket_order, None) + + + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) @@ -693,7 +716,8 @@ def create_purchase_order(**args): "qty": args.qty or 10, "rate": args.rate or 500, "schedule_date": add_days(nowdate(), 1), - "include_exploded_items": args.get('include_exploded_items', 1) + "include_exploded_items": args.get('include_exploded_items', 1), + "against_blanket_order": args.against_blanket_order }) if not args.do_not_save: po.insert() diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index c409c1f46e0..15bc97c2a4d 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -43,7 +43,6 @@ "base_amount", "pricing_rules", "is_free_item", - "is_fixed_asset", "section_break_29", "net_rate", "net_amount", @@ -67,6 +66,7 @@ "supplier_quotation", "supplier_quotation_item", "col_break5", + "against_blanket_order", "blanket_order", "blanket_order_rate", "item_group", @@ -511,6 +511,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", @@ -518,6 +519,7 @@ "options": "Blanket Order" }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", @@ -703,16 +705,14 @@ }, { "default": "0", - "fetch_from": "item_code.is_fixed_asset", - "fieldname": "is_fixed_asset", + "fieldname": "against_blanket_order", "fieldtype": "Check", - "label": "Is Fixed Asset", - "read_only": 1 + "label": "Against Blanket Order" } ], "idx": 1, "istable": 1, - "modified": "2019-11-07 17:19:12.090355", + "modified": "2019-11-19 14:10:52.865006", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index faed707d600..38118bd78d4 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -44,6 +44,8 @@ def make_sales_order(source_name): target.item_name = item.get("item_name") target.description = item.get("description") target.uom = item.get("stock_uom") + target.against_blanket_order = 1 + target.blanket_order = source_name target_doc = get_mapped_doc("Blanket Order", source_name, { "Blanket Order": { @@ -71,6 +73,8 @@ def make_purchase_order(source_name): target.description = item.get("description") target.uom = item.get("stock_uom") target.warehouse = item.get("default_warehouse") + target.against_blanket_order = 1 + target.blanket_order = source_name target_doc = get_mapped_doc("Blanket Order", source_name, { "Blanket Order": { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5da949320a1..46a58fba7cc 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1716,6 +1716,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + against_blanket_order: function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + if(!item.against_blanket_order) { + frappe.model.set_value(this.frm.doctype + " Item", item.name, "blanket_order", null); + frappe.model.set_value(this.frm.doctype + " Item", item.name, "blanket_order_rate", 0.00); + } + }, + blanket_order: function(doc, cdt, cdn) { var me = this; var item = locals[cdt][cdn]; diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index bd078414885..feb6b76c4d3 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -12,6 +12,7 @@ from erpnext.selling.doctype.sales_order.sales_order import make_work_orders from erpnext.controllers.accounts_controller import update_child_qty_rate import json from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request +from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order class TestSalesOrder(unittest.TestCase): def tearDown(self): @@ -819,6 +820,25 @@ class TestSalesOrder(unittest.TestCase): mr_doc = frappe.get_doc('Material Request',mr.get('name')) self.assertEqual(mr_doc.items[0].sales_order, so.name) + def test_so_optional_blanket_order(self): + """ + Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. + Second Sales Order should not add on to Blanket Orders Ordered Quantity. + """ + + bo = make_blanket_order(blanket_order_type = "Selling", quantity = 10, rate = 10) + + so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) + so_doc = frappe.get_doc('Sales Order', so.get('name')) + # To test if the SO has a Blanket Order + self.assertTrue(so_doc.items[0].blanket_order) + + so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) + so_doc = frappe.get_doc('Sales Order', so.get('name')) + # To test if the SO does NOT have a Blanket Order + self.assertEqual(so_doc.items[0].blanket_order, None) + + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) @@ -845,7 +865,8 @@ def make_sales_order(**args): "warehouse": args.warehouse, "qty": args.qty or 10, "uom": args.uom or None, - "rate": args.rate or 100 + "rate": args.rate or 100, + "against_blanket_order": args.against_blanket_order }) so.delivery_date = add_days(so.transaction_date, 10) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 3fd1e6461e1..86b09c28148 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -68,6 +68,7 @@ "target_warehouse", "prevdoc_docname", "col_break4", + "against_blanket_order", "blanket_order", "blanket_order_rate", "planning_section", @@ -574,6 +575,7 @@ "report_hide": 1 }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", @@ -581,6 +583,7 @@ "options": "Blanket Order" }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", @@ -741,11 +744,17 @@ "fieldname": "image_section", "fieldtype": "Section Break", "label": "Image" + }, + { + "default": "0", + "fieldname": "against_blanket_order", + "fieldtype": "Check", + "label": "Against Blanket Order" } ], "idx": 1, "istable": 1, - "modified": "2019-10-10 08:46:26.244823", + "modified": "2019-11-19 14:19:29.491945", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 55f4be136b6..76644ed846c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -213,7 +213,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): project: "", qty: "", stock_qty: "", - conversion_factor: "" + conversion_factor: "", + against_blanket_order: 0/1 } :param item: `item_code` of Item object :return: frappe._dict @@ -302,7 +303,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "weight_per_unit":item.weight_per_unit, "weight_uom":item.weight_uom, "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0, - "transaction_date": args.get("transaction_date") + "transaction_date": args.get("transaction_date"), + "against_blanket_order": args.get("against_blanket_order") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): @@ -996,9 +998,10 @@ def get_serial_no(args, serial_nos=None, sales_order=None): def update_party_blanket_order(args, out): - blanket_order_details = get_blanket_order_details(args) - if blanket_order_details: - out.update(blanket_order_details) + if out["against_blanket_order"]: + blanket_order_details = get_blanket_order_details(args) + if blanket_order_details: + out.update(blanket_order_details) @frappe.whitelist() def get_blanket_order_details(args): From e4274cbfa64d7c364cfd2e0907fda5e459194a70 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 4 Dec 2019 17:51:08 +0530 Subject: [PATCH 39/76] fix: change formatting --- erpnext/public/js/hub/pages/Search.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/hub/pages/Search.vue b/erpnext/public/js/hub/pages/Search.vue index 8ab48d0e8d7..103284289bb 100644 --- a/erpnext/public/js/hub/pages/Search.vue +++ b/erpnext/public/js/hub/pages/Search.vue @@ -42,7 +42,7 @@ export default { computed: { page_title() { return this.items.length - ? __(`Results for "${this.search_value}" ${this.category!=='All'? `in category ${this.category}`: ''}`) + ? __(`Results for "${this.search_value}" ${this.category !== 'All'? `in category ${this.category}` : ''}`) : __('No Items found.'); } }, @@ -52,9 +52,10 @@ export default { methods: { get_items() { if (this.category !== 'All') { - this.filters['hub_category']=this.category; + this.filters['hub_category'] = this.category; } - hub.call('get_items', { keyword: this.search_value, + hub.call('get_items', { + keyword: this.search_value, filters: this.filters }) .then((items) => { From 175d604f5ffd98df2ee46da1592babb807bc433d Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 4 Dec 2019 23:08:51 +0530 Subject: [PATCH 40/76] chore: commonize function --- erpnext/projects/doctype/project/project.js | 77 ++++++--------------- 1 file changed, 21 insertions(+), 56 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 069e3dc6fee..3570a0f2be4 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -4,68 +4,16 @@ frappe.ui.form.on("Project", { setup(frm) { frm.make_methods = { 'Timesheet': () => { - let doctype = 'Timesheet'; - frappe.model.with_doctype(doctype, () => { - let new_doc = frappe.model.get_new_doc(doctype); - - // add a new row and set the project - let time_log = frappe.model.get_new_doc('Timesheet Detail'); - time_log.project = frm.doc.name; - time_log.parent = new_doc.name; - time_log.parentfield = 'time_logs'; - time_log.parenttype = 'Timesheet'; - new_doc.time_logs = [time_log]; - - frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); - }); + open_form(frm, "Timesheet", "Timesheet Detail", "time_logs"); }, 'Purchase Order': () => { - let doctype = 'Purchase Order'; - frappe.model.with_doctype(doctype, () => { - let new_doc = frappe.model.get_new_doc(doctype); - - // add a new row and set the project - let purchase_order_item = frappe.model.get_new_doc('Purchase Order Item'); - purchase_order_item.project = frm.doc.name; - purchase_order_item.parent = new_doc.name; - purchase_order_item.parentfield = 'items'; - purchase_order_item.parenttype = 'Purchase Order'; - new_doc.items = [purchase_order_item]; - - frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); - }); + open_form(frm, "Purchase Order", "Purchase Order Item", "items"); }, 'Purchase Receipt': () => { - let doctype = 'Purchase Receipt'; - frappe.model.with_doctype(doctype, () => { - let new_doc = frappe.model.get_new_doc(doctype); - - // add a new row and set the project - let purchase_receipt_item = frappe.model.get_new_doc('Purchase Receipt Item'); - purchase_receipt_item.project = frm.doc.name; - purchase_receipt_item.parent = new_doc.name; - purchase_receipt_item.parentfield = 'items'; - purchase_receipt_item.parenttype = 'Purchase Receipt'; - new_doc.items = [purchase_receipt_item]; - - frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); - }); + open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items"); }, 'Purchase Invoice': () => { - let doctype = 'Purchase Invoice'; - frappe.model.with_doctype(doctype, () => { - let new_doc = frappe.model.get_new_doc(doctype); - - // add a new row and set the project - let purchase_invoice_item = frappe.model.get_new_doc('Purchase Invoice Item'); - purchase_invoice_item.project = frm.doc.name; - purchase_invoice_item.parent = new_doc.name; - purchase_invoice_item.parentfield = 'items'; - purchase_invoice_item.parenttype = 'Purchase Invoice'; - new_doc.items = [purchase_invoice_item]; - - frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); - }); + open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items"); }, }; }, @@ -171,3 +119,20 @@ frappe.ui.form.on("Project", { }, }); + +function open_form(frm, doctype, child_doctype, parentfield) { + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + // add a new row and set the project + let new_child_doc = frappe.model.get_new_doc(child_doctype); + new_child_doc.project = frm.doc.name; + new_child_doc.parent = new_doc.name; + new_child_doc.parentfield = parentfield; + new_child_doc.parenttype = doctype; + new_doc[parentfield] = [new_child_doc]; + + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); + +} \ No newline at end of file From d98ed8047a9232291a7e2f6540c64bb3e37dbe46 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 6 Dec 2019 12:37:24 +0530 Subject: [PATCH 41/76] fix: travis failing --- .../purchase_order_item/purchase_order_item.json | 13 ++++++++++++- erpnext/setup/doctype/email_digest/email_digest.py | 2 +- erpnext/setup/utils.py | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 15bc97c2a4d..6768dfabfea 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-24 19:29:06", "doctype": "DocType", @@ -43,6 +44,7 @@ "base_amount", "pricing_rules", "is_free_item", + "is_fixed_asset", "section_break_29", "net_rate", "net_amount", @@ -708,11 +710,20 @@ "fieldname": "against_blanket_order", "fieldtype": "Check", "label": "Against Blanket Order" + }, + { + "default": "0", + "fetch_from": "item_code.is_fixed_asset", + "fieldname": "is_fixed_asset", + "fieldtype": "Check", + "label": "Is Fixed Asset", + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2019-11-19 14:10:52.865006", + "links": [], + "modified": "2019-12-06 13:17:12.142799", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 0bcddc21517..4d2d540bbc8 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -283,7 +283,7 @@ class EmailDigest(Document): card.value = card.value *-1 card.value = self.fmt_money(card.value,False if key in ("bank_balance", "credit_balance") else True) - cache.setex(cache_key, card, 24 * 60 * 60) + cache.set_value(cache_key, card, expires_in_sec=24 * 60 * 60) context.cards.append(card) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index d1c206d8b1d..1a86b79ad10 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -106,7 +106,8 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No # expire in 6 hours response.raise_for_status() value = response.json()["rates"][to_currency] - cache.setex(key, value, 6 * 60 * 60) + + cache.set_value(key, value, expires_in_sec=6 * 60 * 60) return flt(value) except: frappe.log_error(title="Get Exchange Rate") From 8ab8376975a86d4735d2366f3d278ba8869c5e4a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 2 Dec 2019 09:37:09 +0530 Subject: [PATCH 42/76] fix: Manufacturing UX, added calendar view for job card --- erpnext/manufacturing/doctype/bom/bom.js | 5 - .../doctype/bom/bom_dashboard.py | 2 +- .../doctype/job_card/job_card.js | 146 +- .../doctype/job_card/job_card.json | 67 +- .../doctype/job_card/job_card.py | 52 +- .../doctype/job_card/job_card_calendar.js | 21 + .../job_card_time_log/job_card_time_log.json | 247 +-- .../production_plan/production_plan.js | 3 +- .../production_plan/production_plan.json | 1618 +++-------------- .../production_plan_dashboard.py | 2 +- .../doctype/work_order/work_order.js | 177 +- .../doctype/work_order/work_order.json | 4 +- .../doctype/work_order/work_order.py | 38 +- .../doctype/work_order/work_order_calendar.js | 2 + .../work_order/work_order_dashboard.py | 3 +- .../work_order_operation.json | 9 +- .../workstation/workstation_dashboard.py | 4 +- 17 files changed, 715 insertions(+), 1685 deletions(-) create mode 100644 erpnext/manufacturing/doctype/job_card/job_card_calendar.js diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 8283fd7e6fc..3acaee4ffbd 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -6,7 +6,6 @@ frappe.provide("erpnext.bom"); frappe.ui.form.on("BOM", { setup: function(frm) { frm.custom_make_buttons = { - 'BOM': 'Duplicate BOM', 'Work Order': 'Work Order', 'Quality Inspection': 'Quality Inspection' }; @@ -91,10 +90,6 @@ frappe.ui.form.on("BOM", { } if(frm.doc.docstatus!=0) { - frm.add_custom_button(__("Duplicate BOM"), function() { - frm.copy_doc(); - }, __("Create")); - frm.add_custom_button(__("Work Order"), function() { frm.trigger("make_work_order"); }, __("Create")); diff --git a/erpnext/manufacturing/doctype/bom/bom_dashboard.py b/erpnext/manufacturing/doctype/bom/bom_dashboard.py index 060cd53ef19..361826e2d0c 100644 --- a/erpnext/manufacturing/doctype/bom/bom_dashboard.py +++ b/erpnext/manufacturing/doctype/bom/bom_dashboard.py @@ -25,5 +25,5 @@ def get_data(): } ], 'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt", - "Purchase Invoice", "Job Card", "Stock Entry"] + "Purchase Invoice", "Job Card", "Stock Entry", "BOM"] } diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 95549d5a248..bc8c22998ae 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -3,6 +3,9 @@ frappe.ui.form.on('Job Card', { refresh: function(frm) { + frappe.flags.pause_job = 0; + frappe.flags.resume_job = 0; + if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) { if (frm.doc.for_quantity != frm.doc.transferred_qty) { frm.add_custom_button(__("Material Request"), () => { @@ -13,44 +16,99 @@ frappe.ui.form.on('Job Card', { if (frm.doc.for_quantity != frm.doc.transferred_qty) { frm.add_custom_button(__("Material Transfer"), () => { frm.trigger("make_stock_entry"); - }); + }).addClass("btn-primary"); } } - if (frm.doc.docstatus == 0) { - frm.trigger("make_dashboard"); + if (frm.doc.docstatus == 0 && frm.doc.for_quantity > frm.doc.total_completed_qty + && (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { + frm.trigger("prepare_timer_buttons"); + } + }, - if (!frm.doc.job_started) { - frm.add_custom_button(__("Start Job"), () => { - let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); - row.from_time = frappe.datetime.now_datetime(); - frm.set_value('job_started', 1); - frm.set_value('started_time' , row.from_time); - frm.save(); - }); - } else { - frm.add_custom_button(__("Complete Job"), () => { - let completed_time = frappe.datetime.now_datetime(); - frm.doc.time_logs.forEach(d => { - if (d.from_time && !d.to_time) { - d.to_time = completed_time; - frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.save(); + prepare_timer_buttons: function(frm) { + frm.trigger("make_dashboard"); + if (!frm.doc.job_started) { + frm.add_custom_button(__("Start"), () => { + if (!frm.doc.employee) { + frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", + fieldname: 'employee'}, d => { + if (d.employee) { + frm.set_value("employee", d.employee); } - }) - }); - } + + frm.events.start_job(frm); + }, __("Enter Value"), __("Start")); + } else { + frm.events.start_job(frm); + } + }).addClass("btn-primary"); + } else if (frm.doc.status == "On Hold") { + frm.add_custom_button(__("Resume"), () => { + frappe.flags.resume_job = 1; + frm.events.start_job(frm); + }).addClass("btn-primary"); + } else { + frm.add_custom_button(__("Pause"), () => { + frappe.flags.pause_job = 1; + frm.set_value("status", "On Hold"); + frm.events.complete_job(frm); + }); + + frm.add_custom_button(__("Complete"), () => { + let completed_time = frappe.datetime.now_datetime(); + frm.trigger("hide_timer"); + + frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), + fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { + frm.events.complete_job(frm, completed_time, data.qty); + }, __("Enter Value"), __("Complete")); + }).addClass("btn-primary"); } }, + start_job: function(frm) { + let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); + row.from_time = frappe.datetime.now_datetime(); + frm.set_value('job_started', 1); + frm.set_value('started_time' , row.from_time); + frm.set_value("status", "Work In Progress"); + + if (!frappe.flags.resume_job) { + frm.set_value('current_time' , 0); + } + + frm.save(); + }, + + complete_job: function(frm, completed_time, completed_qty) { + frm.doc.time_logs.forEach(d => { + if (d.from_time && !d.to_time) { + d.to_time = completed_time || frappe.datetime.now_datetime(); + d.completed_qty = completed_qty || 0; + + if(frappe.flags.pause_job) { + let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; + frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0)); + } else { + frm.set_value('started_time' , ''); + frm.set_value('job_started', 0); + frm.set_value('current_time' , 0); + } + + frm.save(); + } + }); + }, + make_dashboard: function(frm) { if(frm.doc.__islocal) return; frm.dashboard.refresh(); const timer = ` -
+
00 : 00 @@ -58,11 +116,16 @@ frappe.ui.form.on('Job Card', { 00
`; - var section = frm.dashboard.add_section(timer); + var section = frm.toolbar.page.add_inner_message(timer); - if (frm.doc.started_time) { - let currentIncrement = moment(frappe.datetime.now_datetime()).diff(moment(frm.doc.started_time),"seconds"); - initialiseTimer(); + let currentIncrement = frm.doc.current_time || 0; + if (frm.doc.started_time || frm.doc.current_time) { + if (frm.doc.status == "On Hold") { + updateStopwatch(currentIncrement); + } else { + currentIncrement += moment(frappe.datetime.now_datetime()).diff(moment(frm.doc.started_time),"seconds"); + initialiseTimer(); + } function initialiseTimer() { const interval = setInterval(function() { @@ -70,12 +133,12 @@ frappe.ui.form.on('Job Card', { updateStopwatch(current); }, 1000); } - + function updateStopwatch(increment) { var hours = Math.floor(increment / 3600); var minutes = Math.floor((increment - (hours * 3600)) / 60); var seconds = increment - (hours * 3600) - (minutes * 60); - + $(section).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString()); $(section).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString()); $(section).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString()); @@ -88,6 +151,10 @@ frappe.ui.form.on('Job Card', { } }, + hide_timer: function(frm) { + frm.toolbar.page.inner_toolbar.find(".stopwatch").remove(); + }, + for_quantity: function(frm) { frm.doc.items = []; frm.call({ @@ -117,5 +184,22 @@ frappe.ui.form.on('Job Card', { timer: function(frm) { return `` + }, + + set_total_completed_qty: function(frm) { + frm.doc.total_completed_qty = 0; + frm.doc.time_logs.forEach(d => { + if (d.completed_qty) { + frm.doc.total_completed_qty += d.completed_qty; + } + }); + + refresh_field("total_completed_qty"); } -}); \ No newline at end of file +}); + +frappe.ui.form.on('Job Card Time Log', { + completed_qty: function(frm) { + frm.events.set_total_completed_qty(frm); + } +}) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 2c217028d06..156accee74e 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -13,10 +13,18 @@ "column_break_4", "posting_date", "company", + "remarks", + "production_section", + "production_item", + "item_name", "for_quantity", "wip_warehouse", - "timing_detail", + "column_break_12", "employee", + "employee_name", + "status", + "project", + "timing_detail", "time_logs", "section_break_13", "total_completed_qty", @@ -28,12 +36,11 @@ "operation_id", "transferred_qty", "requested_qty", - "project", - "remarks", "column_break_20", - "status", + "barcode", "job_started", "started_time", + "current_time", "amended_from" ], "fields": [ @@ -41,13 +48,14 @@ "fieldname": "work_order", "fieldtype": "Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "Work Order", "options": "Work Order", - "read_only": 1, "reqd": 1, "search_index": 1 }, { + "fetch_from": "work_order.bom_no", "fieldname": "bom_no", "fieldtype": "Link", "label": "BOM No", @@ -91,7 +99,7 @@ "fieldname": "for_quantity", "fieldtype": "Float", "in_list_view": 1, - "label": "For Quantity", + "label": "Qty To Manufacture", "reqd": 1 }, { @@ -109,6 +117,7 @@ { "fieldname": "employee", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Employee", "options": "Employee" }, @@ -198,7 +207,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "Open\nWork In Progress\nMaterial Transferred\nSubmitted\nCancelled\nCompleted", + "options": "Open\nWork In Progress\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted", "read_only": 1 }, { @@ -236,10 +245,52 @@ "label": "Naming Series", "options": "PO-JOB.#####", "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Read Only", + "label": "Employee Name" + }, + { + "fieldname": "production_section", + "fieldtype": "Section Break", + "label": "Production" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fetch_from": "work_order.production_item", + "fieldname": "production_item", + "fieldtype": "Read Only", + "label": "Production Item" + }, + { + "fieldname": "barcode", + "fieldtype": "Barcode", + "label": "Barcode", + "read_only": 1 + }, + { + "fetch_from": "work_order.item_name", + "fieldname": "item_name", + "fieldtype": "Read Only", + "label": "Item Name" + }, + { + "fieldname": "current_time", + "fieldtype": "Int", + "hidden": 1, + "label": "Current Time", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "is_submittable": 1, - "modified": "2019-10-30 01:49:19.606178", + "modified": "2019-12-03 13:08:57.926201", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index e8787ed7db0..9a2aaa5b361 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -194,8 +194,9 @@ class JobCard(Document): if self.total_completed_qty <= 0.0: frappe.throw(_("Total completed qty must be greater than zero")) - if self.total_completed_qty > self.for_quantity: - frappe.throw(_("Total completed qty can not be greater than for quantity")) + if self.total_completed_qty != self.for_quantity: + frappe.throw(_("The total completed qty({0}) must be equal to qty to manufacture({1})" + .format(frappe.bold(self.total_completed_qty),frappe.bold(self.for_quantity)))) def update_work_order(self): if not self.work_order: @@ -271,6 +272,8 @@ class JobCard(Document): self.set_status(update_status) def set_status(self, update_status=False): + if self.status == "On Hold": return + self.status = { 0: "Open", 1: "Submitted", @@ -329,6 +332,7 @@ def make_stock_entry(source_name, target_doc=None): target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) target.calculate_rate_and_amount() target.set_missing_values() + target.set_stock_entry_type() doclist = get_mapped_doc("Job Card", source_name, { "Job Card": { @@ -352,4 +356,46 @@ def make_stock_entry(source_name, target_doc=None): return doclist def time_diff_in_minutes(string_ed_date, string_st_date): - return time_diff(string_ed_date, string_st_date).total_seconds() / 60 \ No newline at end of file + return time_diff(string_ed_date, string_st_date).total_seconds() / 60 + +@frappe.whitelist() +def get_job_details(start, end, filters=None): + events = [] + + event_color = { + "Completed": "#cdf5a6", + "Material Transferred": "#ffdd9e", + "Work In Progress": "#D3D3D3" + } + + from frappe.desk.reportview import get_filters_cond + conditions = get_filters_cond("Job Card", filters, []) + + job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order, + `tabJob Card`.employee_name, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''), + min(`tabJob Card Time Log`.from_time) as from_time, + max(`tabJob Card Time Log`.to_time) as to_time + FROM `tabJob Card` , `tabJob Card Time Log` + WHERE + `tabJob Card`.name = `tabJob Card Time Log`.parent {0} + group by `tabJob Card`.name""".format(conditions), as_dict=1) + + for d in job_cards: + subject_data = [] + for field in ["name", "work_order", "remarks", "employee_name"]: + if not d.get(field): continue + + subject_data.append(d.get(field)) + + color = event_color.get(d.status) + job_card_data = { + 'from_time': d.from_time, + 'to_time': d.to_time, + 'name': d.name, + 'subject': '\n'.join(subject_data), + 'color': color if color else "#89bcde" + } + + events.append(job_card_data) + + return events diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js new file mode 100644 index 00000000000..cf07698ad6a --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js @@ -0,0 +1,21 @@ +frappe.views.calendar["Job Card"] = { + field_map: { + "start": "from_time", + "end": "to_time", + "id": "name", + "title": "subject", + "color": "color", + "allDay": "allDay", + "progress": "progress" + }, + gantt: true, + filters: [ + { + "fieldtype": "Link", + "fieldname": "employee", + "options": "Employee", + "label": __("Employee") + } + ], + get_events_method: "erpnext.manufacturing.doctype.job_card.job_card.get_job_details" +}; diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json index 2aab71dee4f..9dd54dd6182 100644 --- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json +++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json @@ -1,208 +1,57 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2019-03-08 23:56:43.187569", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2019-03-08 23:56:43.187569", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "from_time", + "to_time", + "column_break_2", + "time_in_mins", + "completed_qty" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "from_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": "From 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "From Time" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "to_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": "To 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "To Time" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_2", - "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 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "time_in_mins", - "fieldtype": "Float", - "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": "Time In Mins", - "length": 0, - "no_copy": 0, - "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 - }, + "fieldname": "time_in_mins", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Time In Mins", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "completed_qty", - "fieldtype": "Float", - "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": "Completed Qty", - "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 + "default": "0", + "fieldname": "completed_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Completed Qty", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-03-10 17:08:46.504910", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Job Card Time Log", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "modified": "2019-12-03 12:56:02.285448", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Time Log", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 3b24d0fa0ff..2b168d1d76d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -71,12 +71,13 @@ frappe.ui.form.on('Production Plan', { }, __('Create')); } + frm.page.set_inner_btn_group_as_primary(__('Create')); frm.trigger("material_requirement"); const projected_qty_formula = `
-

+

${__("Projected Quantity Formula")} diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 0be9f1a8eef..af844816e8b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -1,1391 +1,321 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2017-10-29 11:53:09.523362", - "custom": 0, - "default_print_format": "", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "autoname": "naming_series:", + "creation": "2017-10-29 11:53:09.523362", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "naming_series", + "company", + "get_items_from", + "column_break1", + "posting_date", + "filters", + "item_code", + "customer", + "warehouse", + "project", + "column_break2", + "from_date", + "to_date", + "sales_orders_detail", + "get_sales_orders", + "sales_orders", + "material_request_detail", + "get_material_request", + "material_requests", + "select_items_to_manufacture_section", + "get_items", + "po_items", + "material_request_planning", + "include_non_stock_items", + "include_subcontracted_items", + "ignore_existing_ordered_qty", + "column_break_25", + "for_warehouse", + "download_materials_required", + "get_items_for_mr", + "section_break_27", + "mr_items", + "other_details", + "total_planned_qty", + "total_produced_qty", + "column_break_32", + "status", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fetch_if_empty": 0, - "fieldname": "naming_series", - "fieldtype": "Select", - "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": "Naming Series", - "length": 0, - "no_copy": 0, - "options": "MFG-PP-.YYYY.-", - "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 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "MFG-PP-.YYYY.-", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "company", - "fieldtype": "Link", - "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": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "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 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fetch_if_empty": 0, - "fieldname": "get_items_from", - "fieldtype": "Select", - "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": "Get Items From", - "length": 0, - "no_copy": 0, - "options": "\nSales Order\nMaterial Request", - "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 - }, + "fieldname": "get_items_from", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Get Items From", + "options": "\nSales Order\nMaterial Request" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break1", - "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 - }, + "fieldname": "column_break1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fetch_if_empty": 0, - "fieldname": "posting_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": "Posting 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 - }, + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval: doc.__islocal", - "columns": 0, - "depends_on": "eval: doc.get_items_from", - "description": "", - "fetch_if_empty": 0, - "fieldname": "filters", - "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": "Filters", - "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 - }, + "collapsible": 1, + "collapsible_depends_on": "eval: doc.__islocal", + "depends_on": "eval: doc.get_items_from", + "fieldname": "filters", + "fieldtype": "Section Break", + "label": "Filters" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "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": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "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 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.get_items_from == \"Sales Order\"", - "fetch_if_empty": 0, - "fieldname": "customer", - "fieldtype": "Link", - "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": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "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 - }, + "depends_on": "eval: doc.get_items_from == \"Sales Order\"", + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.get_items_from == \"Material Request\"", - "fetch_if_empty": 0, - "fieldname": "warehouse", - "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": "Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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 - }, + "depends_on": "eval: doc.get_items_from == \"Material Request\"", + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.get_items_from == \"Sales Order\"", - "fetch_if_empty": 0, - "fieldname": "project", - "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": "Project", - "length": 0, - "no_copy": 0, - "options": "Project", - "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 - }, + "depends_on": "eval: doc.get_items_from == \"Sales Order\"", + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break2", - "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, + "fieldname": "column_break2", + "fieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "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": "From 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "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": "To 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval: doc.__islocal", - "columns": 0, - "depends_on": "eval: doc.get_items_from == \"Sales Order\"", - "fetch_if_empty": 0, - "fieldname": "sales_orders_detail", - "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": "Sales Orders Detail", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval: doc.__islocal", + "depends_on": "eval: doc.get_items_from == \"Sales Order\"", + "fieldname": "sales_orders_detail", + "fieldtype": "Section Break", + "label": "Sales Orders" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "get_sales_orders", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Get Sales Orders", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "get_sales_orders", + "fieldtype": "Button", + "label": "Get Sales Orders" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "sales_orders", - "fieldtype": "Table", - "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": "Sales Orders", - "length": 0, - "no_copy": 1, - "options": "Production Plan Sales Order", - "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 - }, + "fieldname": "sales_orders", + "fieldtype": "Table", + "label": "Sales Orders", + "no_copy": 1, + "options": "Production Plan Sales Order" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval: doc.__islocal", - "columns": 0, - "depends_on": "eval: doc.get_items_from == \"Material Request\"", - "fetch_if_empty": 0, - "fieldname": "material_request_detail", - "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": "Material Request Detail", - "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 - }, + "collapsible": 1, + "collapsible_depends_on": "eval: doc.__islocal", + "depends_on": "eval: doc.get_items_from == \"Material Request\"", + "fieldname": "material_request_detail", + "fieldtype": "Section Break", + "label": "Material Request Detail" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "get_material_request", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Get Material Request", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "get_material_request", + "fieldtype": "Button", + "label": "Get Material Request" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "material_requests", - "fieldtype": "Table", - "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": "Material Requests", - "length": 0, - "no_copy": 1, - "options": "Production Plan Material Request", - "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 - }, + "fieldname": "material_requests", + "fieldtype": "Table", + "label": "Material Requests", + "no_copy": 1, + "options": "Production Plan Material Request" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "select_items_to_manufacture_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": "Select Items to Manufacture", - "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 - }, + "fieldname": "select_items_to_manufacture_section", + "fieldtype": "Section Break", + "label": "Select Items to Manufacture" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "get_items_from", - "fetch_if_empty": 0, - "fieldname": "get_items", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Get Items For Work Order", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "get_items_from", + "fieldname": "get_items", + "fieldtype": "Button", + "label": "Get Items For Work Order" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "po_items", - "fieldtype": "Table", - "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": "", - "length": 0, - "no_copy": 1, - "options": "Production Plan Item", - "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 - }, + "fieldname": "po_items", + "fieldtype": "Table", + "no_copy": 1, + "options": "Production Plan Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "material_request_planning", - "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": "Material Request Planning", - "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 - }, + "fieldname": "material_request_planning", + "fieldtype": "Section Break", + "label": "Material Request Planning" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fetch_if_empty": 0, - "fieldname": "include_non_stock_items", - "fieldtype": "Check", - "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": "Include Non Stock Items", - "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 - }, + "default": "1", + "fieldname": "include_non_stock_items", + "fieldtype": "Check", + "label": "Include Non Stock Items" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fetch_if_empty": 0, - "fieldname": "include_subcontracted_items", - "fieldtype": "Check", - "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": "Include Subcontracted Items", - "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 - }, + "default": "1", + "fieldname": "include_subcontracted_items", + "fieldtype": "Check", + "label": "Include Subcontracted Items" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "If enabled, then system will create the material even if the raw materials are available", - "fetch_if_empty": 0, - "fieldname": "ignore_existing_ordered_qty", - "fieldtype": "Check", - "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": "Ignore Existing Projected Quantity", - "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 - }, + "default": "0", + "description": "To know more about projected quantity, click here.", + "fieldname": "ignore_existing_ordered_qty", + "fieldtype": "Check", + "label": "Ignore Existing Projected Quantity" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_25", - "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 - }, + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "for_warehouse", - "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": "For Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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 - }, + "fieldname": "for_warehouse", + "fieldtype": "Link", + "label": "For Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "download_materials_required", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Download Materials Required", - "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 - }, + "fieldname": "download_materials_required", + "fieldtype": "Button", + "label": "Download Required Materials" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "get_items_for_mr", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Get Raw Materials For Production", - "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 - }, + "fieldname": "get_items_for_mr", + "fieldtype": "Button", + "label": "Get Raw Materials For Production" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "section_break_27", - "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, - "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 - }, + "fieldname": "section_break_27", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "mr_items", - "fieldtype": "Table", - "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": "Material Request Plan Item", - "length": 0, - "no_copy": 1, - "options": "Material Request Plan Item", - "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 - }, + "fieldname": "mr_items", + "fieldtype": "Table", + "label": "Material Request Plan Item", + "no_copy": 1, + "options": "Material Request Plan Item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fetch_if_empty": 0, - "fieldname": "projected_qty_formula", - "fieldtype": "HTML", - "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": "Projected Qty Formula", - "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 - }, + "collapsible": 1, + "fieldname": "other_details", + "fieldtype": "Section Break", + "label": "Other Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "other_details", - "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": "Other Details", - "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 - }, + "default": "0", + "fieldname": "total_planned_qty", + "fieldtype": "Float", + "label": "Total Planned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "total_planned_qty", - "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 Planned Qty", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "default": "0", + "fieldname": "total_produced_qty", + "fieldtype": "Float", + "label": "Total Produced Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "total_produced_qty", - "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 Produced Qty", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "fieldname": "column_break_32", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_32", - "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 - }, + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled\nMaterial Requested", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Draft", - "fetch_if_empty": 0, - "fieldname": "status", - "fieldtype": "Select", - "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": "Status", - "length": 0, - "no_copy": 1, - "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled\nMaterial Requested", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "fetch_if_empty": 0, - "fieldname": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Production Plan", - "permlevel": 0, - "print_hide": 1, - "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 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Production Plan", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "icon": "fa fa-calendar", - "idx": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-09 12:05:14.300886", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Production Plan", - "name_case": "", - "owner": "Administrator", + ], + "icon": "fa fa-calendar", + "is_submittable": 1, + "links": [], + "modified": "2019-12-04 15:58:50.940460", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "ASC", - "title_field": "", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py index 91c28555d61..09ec24a67a2 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py @@ -6,7 +6,7 @@ def get_data(): 'fieldname': 'production_plan', 'transactions': [ { - 'label': _('Related'), + 'label': _('Transactions'), 'items': ['Work Order', 'Material Request'] }, ] diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 5c721c723d2..8ca89171b66 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -6,6 +6,7 @@ frappe.ui.form.on("Work Order", { frm.custom_make_buttons = { 'Stock Entry': 'Start', 'Pick List': 'Create Pick List', + 'Job Card': 'Create Job Card' }; // Set query for warehouses @@ -131,7 +132,8 @@ frappe.ui.form.on("Work Order", { } if (frm.doc.docstatus===1) { - frm.trigger('show_progress'); + frm.trigger('show_progress_for_items'); + frm.trigger('show_progress_for_operations'); } if (frm.doc.docstatus === 1 @@ -179,89 +181,72 @@ frappe.ui.form.on("Work Order", { make_job_card: function(frm) { let qty = 0; - const fields = [{ - fieldtype: "Link", - fieldname: "operation", - options: "Operation", - label: __("Operation"), - get_query: () => { - const filter_workstation = frm.doc.operations.filter(d => { - if (d.status != "Completed") { - return d; - } - }); + let operations_data = []; - return { - filters: { - name: ["in", (filter_workstation || []).map(d => d.operation)] - } - }; - }, - reqd: true - }, { - fieldtype: "Link", - fieldname: "workstation", - options: "Workstation", - label: __("Workstation"), - get_query: () => { - const operation = dialog.get_value("operation"); - const filter_workstation = frm.doc.operations.filter(d => { - if (d.operation == operation) { - return d; - } - }); - - return { - filters: { - name: ["in", (filter_workstation || []).map(d => d.workstation)] - } - }; - }, - onchange: () => { - const operation = dialog.get_value("operation"); - const workstation = dialog.get_value("workstation"); - if (operation && workstation) { - const row = frm.doc.operations.filter(d => d.operation == operation && d.workstation == workstation)[0]; - qty = frm.doc.qty - row.completed_qty; - - if (qty > 0) { - dialog.set_value("qty", qty); - } - } - }, - reqd: true - }, { - fieldtype: "Float", - fieldname: "qty", - label: __("For Quantity"), - reqd: true - }]; - - const dialog = frappe.prompt(fields, function(data) { - if (data.qty > qty) { - frappe.throw(__("For Quantity must be less than quantity {0}", [qty])); + const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), + fields: [ + { + fieldtype:'Link', + fieldname:'operation', + label: __('Operation'), + read_only:1, + in_list_view:1 + }, + { + fieldtype:'Link', + fieldname:'workstation', + label: __('Workstation'), + read_only:1, + in_list_view:1 + }, + { + fieldtype:'Data', + fieldname:'name', + label: __('Operation Id') + }, + { + fieldtype:'Float', + fieldname:'pending_qty', + label: __('Pending Qty'), + }, + { + fieldtype:'Float', + fieldname:'qty', + label: __('Quantity to Manufacture'), + read_only:0, + in_list_view:1, + }, + ], + data: operations_data, + in_place_edit: true, + get_data: function() { + return operations_data; } - - if (data.qty <= 0) { - frappe.throw(__("For Quantity must be greater than zero")); - } - + }, function(data) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", args: { work_order: frm.doc.name, - operation: data.operation, - workstation: data.workstation, - qty: data.qty - }, - callback: function(r){ - if (r.message) { - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); - } + operations: data.operations, } }); - }, __("For Job Card")); + }, __("Job Card"), __("Create")); + + var pending_qty = 0; + frm.doc.operations.forEach(data => { + if(data.completed_qty != frm.doc.qty) { + pending_qty = frm.doc.qty - flt(data.completed_qty); + + dialog.fields_dict.operations.df.data.push({ + 'name': data.name, + 'operation': data.operation, + 'workstation': data.workstation, + 'qty': pending_qty, + 'pending_qty': pending_qty, + }); + } + }); + dialog.fields_dict.operations.grid.refresh(); }, make_bom: function(frm) { @@ -277,7 +262,7 @@ frappe.ui.form.on("Work Order", { }); }, - show_progress: function(frm) { + show_progress_for_items: function(frm) { var bars = []; var message = ''; var added_min = false; @@ -311,6 +296,44 @@ frappe.ui.form.on("Work Order", { frm.dashboard.add_progress(__('Status'), bars, message); }, + show_progress_for_operations: function(frm) { + if (frm.doc.operations && frm.doc.operations.length) { + + let progress_class = { + "Work in Progress": "progress-bar-warning", + "Completed": "progress-bar-success" + }; + + let bars = []; + let message = ''; + let title = ''; + let status_wise_oprtation_data = {}; + let total_completed_qty = frm.doc.qty * frm.doc.operations.length; + + frm.doc.operations.forEach(d => { + if (!status_wise_oprtation_data[d.status]) { + status_wise_oprtation_data[d.status] = [d.completed_qty, d.operation]; + } else { + status_wise_oprtation_data[d.status][0] += d.completed_qty; + status_wise_oprtation_data[d.status][1] += ', ' + d.operation; + } + }); + + for (let key in status_wise_oprtation_data) { + title = __("{0} Operations: {1}", [key, status_wise_oprtation_data[key][1].bold()]); + bars.push({ + 'title': title, + 'width': status_wise_oprtation_data[key][0] / total_completed_qty * 100 + '%', + 'progress_class': progress_class[key] + }); + + message += title + '. '; + } + + frm.dashboard.add_progress(__('Status'), bars, message); + } + }, + production_item: function(frm) { if (frm.doc.production_item) { frappe.call({ diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 0d073a2312b..6152fbb8402 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-01-10 16:34:16", @@ -468,7 +469,8 @@ "idx": 1, "image_field": "image", "is_submittable": 1, - "modified": "2019-08-28 12:29:35.315239", + "links": [], + "modified": "2019-12-04 11:20:04.695123", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index ff489542f66..227ef787cac 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -6,7 +6,7 @@ import frappe import json import math from frappe import _ -from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate +from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict from dateutil.relativedelta import relativedelta @@ -755,21 +755,41 @@ def query_sales_order(production_item): return out @frappe.whitelist() -def make_job_card(work_order, operation, workstation, qty=0): +def make_job_card(work_order, operations): + if isinstance(operations, string_types): + operations = json.loads(operations) + work_order = frappe.get_doc('Work Order', work_order) - row = get_work_order_operation_data(work_order, operation, workstation) - if row: - return create_job_card(work_order, row, qty) + for row in operations: + validate_operation_data(row) + create_job_card(work_order, row, row.get("qty"), auto_create=True) + +def validate_operation_data(row): + if row.get("qty") <= 0: + frappe.throw(_("Quantity to Manufacture can not be zero for the operation {0}") + .format( + frappe.bold(row.get("operation")) + ) + ) + + if row.get("qty") > row.get("pending_qty"): + frappe.throw(_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})") + .format( + frappe.bold(row.get("operation")), + frappe.bold(row.get("qty")), + frappe.bold(row.get("pending_qty")) + ) + ) def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): doc = frappe.new_doc("Job Card") doc.update({ 'work_order': work_order.name, - 'operation': row.operation, - 'workstation': row.workstation, + 'operation': row.get("operation"), + 'workstation': row.get("workstation"), 'posting_date': nowdate(), 'for_quantity': qty or work_order.get('qty', 0), - 'operation_id': row.name, + 'operation_id': row.get("name"), 'bom_no': work_order.bom_no, 'project': work_order.project, 'company': work_order.company, @@ -785,7 +805,7 @@ def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto doc.schedule_time_logs(row) doc.insert() - frappe.msgprint(_("Job card {0} created").format(doc.name)) + frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name))) return doc diff --git a/erpnext/manufacturing/doctype/work_order/work_order_calendar.js b/erpnext/manufacturing/doctype/work_order/work_order_calendar.js index c44b1e2700d..7ce05e9b881 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_calendar.js +++ b/erpnext/manufacturing/doctype/work_order/work_order_calendar.js @@ -2,11 +2,13 @@ // For license information, please see license.txt frappe.views.calendar["Work Order"] = { + fields: ["planned_start_date", "planned_end_date", "status", "produced_qty", "qty", "name", "name"], field_map: { "start": "planned_start_date", "end": "planned_end_date", "id": "name", "title": "name", + "status": "status", "allDay": "allDay", "progress": function(data) { return flt(data.produced_qty) / data.qty * 100; diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py index 0d3c30ea6eb..87c090f99c3 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py +++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py @@ -6,7 +6,8 @@ def get_data(): 'fieldname': 'work_order', 'transactions': [ { - 'items': ['Pick List', 'Stock Entry', 'Job Card'] + 'label': _('Transactions'), + 'items': ['Stock Entry', 'Job Card', 'Pick List'] } ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 75d42cd061e..3f5e18e8130 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2014-10-16 14:35:41.950175", "doctype": "DocType", "editable_grid": 1, @@ -68,6 +69,7 @@ "description": "Operation completed for how many finished goods?", "fieldname": "completed_qty", "fieldtype": "Float", + "in_list_view": 1, "label": "Completed Qty", "no_copy": 1, "read_only": 1 @@ -188,8 +190,9 @@ } ], "istable": 1, - "modified": "2019-07-16 23:01:07.720337", - "modified_by": "govindsmenokee@gmail.com", + "links": [], + "modified": "2019-12-03 19:24:29.594189", + "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", "owner": "Administrator", @@ -197,4 +200,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py index 7f0124b5030..3ddbe731700 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py +++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py @@ -13,5 +13,7 @@ def get_data(): 'label': _('Transaction'), 'items': ['Work Order', 'Job Card', 'Timesheet'] } - ] + ], + 'disable_create_buttons': ['BOM', 'Routing', 'Operation', + 'Work Order', 'Job Card', 'Timesheet'] } From ad53649f41a6c2caa1bdf04514b1308ec95ade4c Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 6 Dec 2019 19:34:49 +0530 Subject: [PATCH 43/76] fix: Validation msg fix in GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.js | 27 +++++++++++++++------ erpnext/regional/report/gstr_1/gstr_1.py | 31 ++++++++++++------------ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index ce559218cbd..091dfd12532 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -55,14 +55,25 @@ frappe.query_reports["GSTR-1"] = { report.page.add_inner_button(__("Download as JSON"), function () { var filters = report.get_values(); - const args = { - cmd: 'erpnext.regional.report.gstr_1.gstr_1.get_json', - data: report.data, - report_name: report.report_name, - filters: filters - }; - - open_url_post(frappe.request.url, args); + frappe.call({ + method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', + args: { + data: report.data, + report_name: report.report_name, + filters: filters + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.report.gstr_1.gstr_1.download_json_file', + data: r.message.data, + report_name: r.message.report_name, + report_type: r.message.report_type + }; + open_url_post(frappe.request.url, args); + } + } + }) }); } } diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 090616b0777..4f9cc7ff7a0 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -532,16 +532,9 @@ class Gstr1Report(object): self.columns = self.invoice_columns + self.tax_columns + self.other_columns @frappe.whitelist() -def get_json(): - data = frappe._dict(frappe.local.form_dict) - - del data["cmd"] - if "csrf_token" in data: - del data["csrf_token"] - - filters = json.loads(data["filters"]) - report_data = json.loads(data["data"]) - report_name = data["report_name"] +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) gstin = get_company_gstin_number(filters["company"]) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) @@ -575,7 +568,11 @@ def get_json(): out = get_export_json(res) gst_json["exp"] = out - download_json_file(report_name, filters["type_of_business"], gst_json) + return { + 'report_name': report_name, + 'report_type': filters['type_of_business'], + 'data': gst_json + } def get_b2b_json(res, gstin): inv_type, out = {"Registered Regular": "R", "Deemed Export": "DE", "URD": "URD", "SEZ": "SEZ"}, [] @@ -722,11 +719,15 @@ def get_company_gstin_number(company): if gstin: return gstin[0]["gstin"] else: - frappe.throw(_("Please set valid GSTIN No. in Company Address")) + frappe.throw(_("Please set valid GSTIN No. in Company Address for company {0}".format( + frappe.bold(company) + ))) -def download_json_file(filename, report_type, data): +@frappe.whitelist() +def download_json_file(): ''' download json content in a file ''' - frappe.response['filename'] = frappe.scrub("{0} {1}".format(filename, report_type)) + '.json' - frappe.response['filecontent'] = json.dumps(data) + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0} {1}".format(data['report_name'], data['report_type'])) + '.json' + frappe.response['filecontent'] = data['data'] frappe.response['content_type'] = 'application/json' frappe.response['type'] = 'download' From 99309229bc082fb1f929f661061f5f9eb1252442 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 6 Dec 2019 19:59:54 +0530 Subject: [PATCH 44/76] fix: Add missing semicolon --- erpnext/regional/report/gstr_1/gstr_1.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 091dfd12532..1a7ff2bf5a6 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -73,7 +73,7 @@ frappe.query_reports["GSTR-1"] = { open_url_post(frappe.request.url, args); } } - }) + }); }); } } From 690068142b786f16045ffca0caa8423e7e07af42 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 6 Dec 2019 20:58:26 +0530 Subject: [PATCH 45/76] feat: Continue Slide and New Illustrations for Onboarding Wizard (#19813) * feat: added illustrations for onboarding * refactor: change help label for supplier * refactor (breaking): rename Setup Wizard slide to Onboarding Slide * chore: code cleanup for the old doctype Setup Wizard Slide * fix: added a 'Continue' type slide * fix: change text in continue slide * fix: change text for continue slide --- .../add_a_few_suppliers.json | 11 ++++--- .../images/illustrations/collaboration.png | Bin 3849 -> 0 bytes .../public/images/illustrations/customer.png | Bin 4093 -> 0 bytes .../illustrations/customers-onboard.png | Bin 0 -> 16870 bytes .../images/illustrations/desk-onboard.png | Bin 0 -> 11054 bytes .../illustrations/letterhead-onboard.png | Bin 0 -> 63859 bytes .../images/illustrations/letterhead.png | Bin 1613 -> 0 bytes .../public/images/illustrations/onboard.png | Bin 2742 -> 0 bytes .../public/images/illustrations/product.png | Bin 3136 -> 0 bytes .../images/illustrations/products-onboard.png | Bin 0 -> 33014 bytes .../images/illustrations/supplier-onboard.png | Bin 0 -> 17531 bytes .../public/images/illustrations/supplier.png | Bin 3531 -> 0 bytes erpnext/public/images/illustrations/user.png | Bin 7887 -> 0 bytes .../add_a_few_customers.json | 11 ++++--- .../welcome_back_to_erpnext!.json | 23 ++++++++++++++ .../welcome_to_erpnext!.json | 10 +++---- .../add_a_few_products_you_buy_or_sell.json | 28 +++++++----------- 17 files changed, 49 insertions(+), 34 deletions(-) rename erpnext/buying/{setup_wizard_slide => onboarding_slide}/add_a_few_suppliers/add_a_few_suppliers.json (78%) delete mode 100644 erpnext/public/images/illustrations/collaboration.png delete mode 100644 erpnext/public/images/illustrations/customer.png create mode 100644 erpnext/public/images/illustrations/customers-onboard.png create mode 100644 erpnext/public/images/illustrations/desk-onboard.png create mode 100644 erpnext/public/images/illustrations/letterhead-onboard.png delete mode 100644 erpnext/public/images/illustrations/letterhead.png delete mode 100644 erpnext/public/images/illustrations/onboard.png delete mode 100644 erpnext/public/images/illustrations/product.png create mode 100644 erpnext/public/images/illustrations/products-onboard.png create mode 100644 erpnext/public/images/illustrations/supplier-onboard.png delete mode 100644 erpnext/public/images/illustrations/supplier.png delete mode 100644 erpnext/public/images/illustrations/user.png rename erpnext/selling/{setup_wizard_slide => onboarding_slide}/add_a_few_customers/add_a_few_customers.json (77%) create mode 100644 erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json rename erpnext/setup/{setup_wizard_slide => onboarding_slide}/welcome_to_erpnext!/welcome_to_erpnext!.json (60%) rename erpnext/stock/{setup_wizard_slide => onboarding_slide}/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json (76%) diff --git a/erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json b/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json similarity index 78% rename from erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json rename to erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json index 006d139eb02..d3adcb79812 100644 --- a/erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json +++ b/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json @@ -3,18 +3,18 @@ "app": "ERPNext", "creation": "2019-11-15 14:45:32.626641", "docstatus": 0, - "doctype": "Setup Wizard Slide", + "doctype": "Onboarding Slide", "domains": [], "help_links": [ { - "label": "Supplier", + "label": "Learn More", "video_id": "zsrrVDk6VBs" } ], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/supplier.png", + "image_src": "/assets/erpnext/images/illustrations/supplier-onboard.png", "max_count": 3, - "modified": "2019-11-26 18:26:25.498325", + "modified": "2019-12-03 22:53:50.552445", "modified_by": "Administrator", "name": "Add A Few Suppliers", "owner": "Administrator", @@ -44,6 +44,5 @@ ], "slide_order": 50, "slide_title": "Add A Few Suppliers", - "slide_type": "Create", - "submit_method": "" + "slide_type": "Create" } \ No newline at end of file diff --git a/erpnext/public/images/illustrations/collaboration.png b/erpnext/public/images/illustrations/collaboration.png deleted file mode 100644 index 12c67e394ccf22e12ff8e0c149139e82e73b321f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3849 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 GfAc@J)HmJ$ diff --git a/erpnext/public/images/illustrations/customer.png b/erpnext/public/images/illustrations/customer.png deleted file mode 100644 index b2ddbf3bb4709407ef493926e3490336568f6543..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4093 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 diff --git a/erpnext/public/images/illustrations/customers-onboard.png b/erpnext/public/images/illustrations/customers-onboard.png new file mode 100644 index 0000000000000000000000000000000000000000..4a517bde29c071b1d7866c2e02aa7115aa0fac60 GIT binary patch literal 16870 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 P7CyXFeOo1K68b*?@SBA4 literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/desk-onboard.png b/erpnext/public/images/illustrations/desk-onboard.png new file mode 100644 index 0000000000000000000000000000000000000000..74b632dc30b652d250a1077ce4f0735cf0cffc0c GIT binary patch literal 11054 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 literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/letterhead.png b/erpnext/public/images/illustrations/letterhead.png deleted file mode 100644 index 37df6d7f6fce07fd2604c4c8387dd18869b9bc54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1613 zcmZ`(do>*s zFI0QlN*gjEZq5z>#U~Aj(o7+m;>!V0Y5EyR_Bs_EX;Gf*LUojXtE8bWCtFQ2>IPV% zE zAO>neRn`oW5hBZ7VXw?QZYNlZm{Bz(XXDL+X#NI4iiQD4guQVGUS){JKThOYi~i~u zUKn9?=5jid-;{E~I+I2oX>@1nx3d-XB?-)!;D$=eG#gv?eBS$e$mnw33+4y?n{T_V z@ogT8r~Xvxy{}`BEG@wX3%|C-;@ebW80<+cyOpuljrCbG#yrvWFI#N(e6GP2Of(KD z>^xLx5x>_&B@p`6?S={rG5inv+jpd%`W5J{#fCC3Sv?0&C7zK-G5!&K5#eE7AXZeN z&ufwHzN8M%5x-eY>v^>dQ*VF4zU|~+w0#uz-I%k!Wp(x4S^;Y)awYNFS5C*W^wZNV zQ3w4R%evU&4#)Jzl24bO4KJot)p+#Se_)2L2Kv5sokB-da!!?~v!`?LVxIy;ln-AT zI<`s1c*=VTjC{&9ZMf{VBj9FfOl{ubgf(X>-}&cQ@m1>A&g)s;4@nUCxCb^{zB3;i zcmA;jAzL z2HFtvu#_o8g_ls@yN8}VKn7^1=0^w8vT9#3=4RP;j9b|WN24MiXZ*edQhl53Iz_{^ zx4b8lNO!XvjIhUjG+{QYW%IK5WTmqQ`??EVX7 z!aJTj!xBlCpuZ){W>H;Iw{dwOl%;>J>{fbx#sbRDU3{LL#;bGBbkZTGSk{YZUbC&e z7sxY+ciitmbBfX}21mjp(o2B*QmPrkPlM z6i!d^QncG4(~|5b$FD%Th;UUo(`OJFRXH-2{b0{`S(yGr1RAp4k1Js4*K5&cv&C&CZ>VDdvZ+-k zwN|V&ox2%4w@P6-@ONQ24Nk5Lq?ieki)7@5$1Q= z0C5^F*93GABA5%yRQa);lW6z$D;e@tLFY$(4QBS|$&{`D<0&_E;sxb|?ouu2lB4`% zJy%=kjVHHFK5SDYZNmpZSFNjC{xpBF;^;bTnMHrR_*Wxr=Imz#GvKF-4kAWaDR|}} zdXfB!Wx&K-WdKD-&lE$J0P{UiMA=?QGR^8Y%@@ZM#3qs|anxZGV@FO^pQfPD^F_(| z`xSQm@(3Ib_R}ppH^J}KRrI$-EtL_cqt2U}e4ES7{Bb2oJLy)~{JEl@d|}cx0b6wsGb*4tW{-EP4c22v&$Fc)FJOUEyYd!?)Krs+t8DPX9_=9!lp4MwzWI_3@ zFSLi>Y7zr2+!70mio$-$Hx7sE!j7(P!CV3{94=E5^NSuo`*CD#%jF3089qHWAU@n8SJBA@8AQH^2P0h_si3D%+O(cRPiD0pg zKp+ta8DF;N{!0)U70L|T|Nnx;l2M#gp!L(iD=Li33yxs`kH^C^_wD8E4vuEvqhiAI SCTz8(L*U}*;c#m!dgyN&#IyVW diff --git a/erpnext/public/images/illustrations/onboard.png b/erpnext/public/images/illustrations/onboard.png deleted file mode 100644 index 094aa3f8ddfc1a07762df08cb3c134ae3ce19384..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2742 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 diff --git a/erpnext/public/images/illustrations/product.png b/erpnext/public/images/illustrations/product.png deleted file mode 100644 index f864b7af60ef44849084225da7f601bcbb33a7b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3136 zcmZ`*cRbXO|9=~2+~MpU5h6!-N4V@6cb$2Zy`6K}JEfD6aaMykQb^8@l2JK(%P5qP zkm}>ih=i~H{QdF!6J_Haevmr(jLl;PxA=GoTF};5g?DD!mI=Aa;1E0TF zuy}3I>euY64gA`_d`e_quWwkQSFSdE)A_Wa8StOMt@g})S=&GRwp#>VqTs9{Z}W{B zh$99;2ck&4T7Pyd`cf{GthsTpCV<;g&STm#iVgO{RhT@6YaGI!;kei3?{Tb|$=qr9 zqaA*KRA=&9rM?Qdd=k`ZuK&<(F4bURE^TQ-V{%n9{;!?({ZWKR-lHlZzd`p$$uGHg zvquhY%WI1Gw0Ox$f@KH4ixI7=7GtUSW3o6e?16<`Cg%FSXsWLWF?*;)Vc%&Yugy$q z*nfl$rQ&dOCzUO^0)KbkKh3f$z!}i%kFEq)PTFhO0^sAJ^q5( zrd%tfE#s5U3-ONg+REcYNExM=Qj#p$J5X()zr96D7(BDlczZP zR-XZTYGRcJZa;(Sjx69UIE$;aZjtBHi8~8Q8|fr!m6IbxXFvmA?i> zz5?!$XVHIb;A=85Kt%?LtpgMH($S!63_Yf5*HMQ`V?Si`&iyPoH2lcZRpeL z@)-dnmFyNhC)5JAdrtvrEsUq(uU|gU&*)Z{ zl)F*|#615nOa8(Nd&~!vOm~8PwwtjMEe#K^9j{F;A{p87;4;Gd%XcQVXHPqnt zl4Wnyz=0l~vSQW<`+F0{ua#W++U*nPJHaIPl11b@N?^e*KQNl!hQ7JGC^#U8GzQ7R1{;kow58UyR4JxSzYC9l zb+BINr>H7#)s2XKXBm0oZa5D49@f5X;IvE)Jd={$U1^>0R7SXtIL z*cZYqv8m4~CsdQMFr{kZXvU)X#j>B)Ap`WDy;$)oFyITlF@j$3lO~YIj*egYqROin zbY4Iiq7tpCEk7K`;@#QAld*cR`_L}oS##!w06ul zy#pFr44?C0Ws1AWQS!;4UlUViE_~$B8J44x7n@YJ?VqtNw=5+eqk(4K09`o|#D%hx zFgabnL+ymNGH$Lm^iTL7^ZlH48PQXfKRwCOCA#Cnuoijyr^aFIDA`k>viYhjKfFkY zqsBa>#Z@*opJv%vnF+A-2PF#~&kMM%9?VO}$qqeL-=-^-WE9DLiJ$kR54M)p-IpC2 z#RZFyGJajSQ7uHJi5-IhQ*VGmErnF?Q{`e$P3-ZT;)#9LcC{OqH@f!-jFF@1Xtnx6 z{u;Y`N7s+LH+31U* zD(r_7{}hv4H&o&)=Y5(~>Nh8tlkA)PY#5-%M9+F9;a(>3Ha%CqCI(vk>axH$4S(C3 z4oRpy{NWxmWs~GhJ|kl4!7~4pf}0~crV9j$(V$hZh3kv)J<9HrR1A&+Wg3<~?yM`966$E(~Z zywN)MR}jrG9Ce4{pxSr5JLRi9ljbgHQ50#&5GOX9jx-e2Rkk+yVF})Q5^*TYwCSGw zLG+6j%mu?!i=;@7vAMFMQlep;t&%b+3b4`NEK~Jllw9w&T|uv*H2hwXZ^SB5>*;x_ z9XGTN5`8J_p~2ZYmOj@fY!7U8-pgBJpTJ$B6kcxfD(x|Td!C!^wf#I|GL18-IQ$u| z&-@q9Vxpc?)MxeywI>-)vlosa_s*&TU5B+^ue6{|g%voK07 zb~9u<1v+#5SmH|q0qsg}4qc@ii5){dRJX_`%{;H~-^`(`F4RFV5Ox2JRLmmMhP}my z5Lt<#fA5tT{uU$<@~J_y7a^AqJ1(^|IGnoliO{(cIBN^&VO;LkGhN;9z&Q@1r}5i^ z0#CANe(91iAGctgxg4)D9^~+q){YagjncBJ?&Se*ub6}q^wc4b@+GO`tU~W4F5LZi zb4q$xRse@UO%~@FpQ~if)E~A|_IcAx)N$is@fYc2V(I@~y?E}~beYB8zcf(SvJi0YM7UMnD<4`5bqNJkZ=S_34u^TB5-i^iwKR2DjJG#_(eFJ zDO zg<)W-V^E&;5Z=$ZjZ~$LWo2Q$zNe93U_;Dc;NMfQ??U)4Ffbo-KKxG(tY;3~|DPuR zuh3DxS_uY52u4~=__r(UaT{{$H)$n0O(%;ch(}wiciZITH>-x_9HiyNP|B9>lKid3 z(Z<{!pqeqf9SPp`f^|M67=8b;LLEPV*~^SHY1=O}SB zlK#$GlL^oc6soH;g}~k_VO+I(G7Q^His9tvvfGDG+1BZsM(o5x$bEYUe0%}~h$6yP z+3N;NqMmJ;iVuuyKjC!4kr0oH!6D;Nk^wrKYdSjCF{uh-jbQTX!C=I#gM_a}|=jO%=ogjyDIZg)KFX(Uuis z-ye%uSLI{j&X<(-f^KgD^tpz%6Dy+g$C3;1$Agyjs*F`D6?AvNL_Su!OoI`5n{%;C zHdJKMV2a&%dJM+huCe!0PS>SMu~hjBub~+y#^r%g+met^(u*Wd#mpmFAF+CYL1AM%aZB>2s9 zVH4+SK>VERzM|QrvfzCG$^hJ&h10y|T7BWRx^BL`)c_)bdBt}ml0}L?)EZR_u?mtO z1!9&lMY;%(zOywILry?Ts`}%Ieg$jGrfEG;0Ut++!VCL2A~w=Z1;mfemdyG4q^|1* z)RS7$aayV5TFh{tnTgq8duZUyaOsHMK?N3sCB+k&nV3r$J^%i71w{stO0F^oNM~Qa z3cuY@#(*lI`SW@tyl~I}dZW3fH@i}@FOue6zw(qddh#kMO+3E};v34=(cmPlG;Zp_ zgashY@SY`SzW^ygH_7r zM#VqE5uh5+x6xSdo|jk5_1*`U27Nkq+u)#)K~m_S-R%v$u zzdarA%vai)O3Uc9AEYDFuI53iqX6)lPm8l;vQMl0qGAv1cl6w4%XXuTWriUoCT69I z|2j4*Mj?^KflAAZ!HD)iJ;dA#!^xF~y@VA{UUvbN_+J%6BIdHqY83ITu!wJRY5B2x%!v8)_^OuY(`6dB zjWkP(uYFxRqo<@Z^^c~iv&7Qt1gW?V`XVyBnF$Bd*k^kSFU(SPT{m}XdzkT&Vl1k9 zFbGfcAgxd!V}h$TQBMT6xu%haJ@jx`WUZdax=~M*)1$gI2h~-I&Zmg5J6HVb11F-W zYSMVX3szUW^gKuv6kFvFkuDJKZ@&8Q>3dPCFRU4YSB(y#0qWDSiQp6SLoZFRq}TVp zHKWY8jxQ!e(dc#y;^?Elj^Qr@NPkU7HbfzI#Q9AnYdJ5O1ZQ?0``s++VQ+QFiXga< zO7erY$%OEG@jCANLcT`?OpezqNiMTn!%95%qU+LuAjKMc|311Fvo+aLp|%brL0t` zzDL0%Aj(SzM%NPtdj@&#+50;Fm?;C_mu5?3ZU8xPBLN*-egmIrNa+IsUH}YJxDCdo z+CxZS#-Y=`)D^$$&q1yf*g|!=LD-NzCbR{^ari%+<&ctGjqORv{5oqg;;T(_SP z%)MMzjbWMLHVXBlB*r&YtW+`yrWSNGE;bTDzN_0N+%Wn5NsaV83FDG!yh3cNHtXMF zd%JIg|L=~E#E~}{ROyK)V@|cJ6`|V9ti=7agylGagutLi_O!O|1x**QV%XF;Y(J_N z%C3RilLG3@)&TK_K&aVmS6^zMz%pu>+bvoUvv!6Kn^`p6;##+*jNb;F)al+SLDklQ z-^;kRk(U8mkzdf=KPb|E!iQDf--}G9<5>Ew46eR>)ZDOkx^2d%I>-yRx4IaOf=Xp* zr#4Pe0!lpqDi3=T1=26fdvJ`p+>~2Q=Q12=h_d>gj+E%90=W*TFmOv@r#c(B7{9hV z|M@Yj$`w-ryHdT)n9Yg*!V{sVN=KfJv=v*iEUxJKF4*G5viU6vIp->%qSR(!Y)utS z`Pzw*iOk}63k91Zak<)&3)$f(U$iG`o{!Oxp)T95HEhPiGz9C~{sz%=t+fBv4g4`F z{atvL%Ap7u9bYj#fB@Es?5=z~gXe`G-|%!pLN5oEy+6+i5r=?DqH4s>sDMeaF*Nc^ zHA*N_$(MjMHW<2;7AkP7SI5^0J~}mi`4(=tlDIWj;!u+YyI6&Vv-SfrCd?i;m&wtz z$x@kG#GY&Yf4+TEZnL(5AX}^}P+~h73J!J-5MWaMR#H}S}K+gTn+V%74EmLj;YKLD0e!A)^CfarFpi-wDf7hR`mAts&`B$s25lg@oz zC@7U@Wm>avYrLYehuI>z0wX_3bZG}z-(AN3WTb%2dAEAt>ipGFQLK{I#jm9`^JTUe zlvq>k+525#mW{#95jx!k+vz&3mJT~){F6BI6f^qYfO=>mK%m3U!Qo0^a{cg{^wVA% zaJHbuskypl;5vFNs=qey?_QciWsoMf>;$~LK0V(*qbXXso?xAf8~Pf!JH<~~<dqV!}Tsl-3s*np9DIUKiWugq>L|H-bItL8TLDu22B5@9?12^3cUu zH{KMH*iz_RecY!tUOIE?u&}T=s_@U5Q5)b-_^|Akus?a(|8RNz;da5E-rnZ|^e|p} zr}l|E8#|XTvrkOOXWrv}nU-jNyw)t&s-mZJ_2q>O8_Fe$)9b15qtJ_wXBSUM5_C~H z6w4FdE-F=NS-(;&pz_F@Df=~5gO4$lE%UDvY=5THotkcDBWp<(w`V*bkKCSxq84`C zMfrWB*porsc&qNoKYqvN>kU6FOGwP0$pLqXKw>nNZN5eQKNhUpEIfM&9BxG^i!ca{ z8HIz`0S4|ylfXney!O~RH8b2F`9N$vdBkgy)iwND!LexR{n|sZBPR*(=@}_CM1MZW zR5zW}s52+zHbIUR`A#~kL+X#ZG;2vZIqgXSxt|d2e#P+B5m%T=7CYq@e)v(?U(D1% z#O&4&5jI%+P}qr1-!4(`K~FOBJyvQcB$xEkE$Mo#_RiSxd)kCEKnIOs#L#qaX0{7b zwb)&NIz7Uk4Vx#qlk;1R_t*gIv&~+yAYaPVFo~Oi=r8T!D<{W%lo?#EB_OiNv3I0E zTA(H^In$nnfN}oe_E5qE&)$l|oBrtI7YtYYk~z|H?E}jbPbe!t7lCCMH}M1m^hfZ_ z4S~QMwP`T|(Uxuj>Ff$aFFNh~sp*1Y*@kzi!{>m?^W1}LHU7>9 zv(1Q@4jPfpE=Tvu3EfFEy8-MEvjJ&M> z_^veJg0eC*mNO3pH0M(->UA?#DVqutmd0gtkG~K0KS<~vvtxY=G|S=d1smJ5WF_{^ zq<#x<>2=k_%jKw_3$KLp7yH|VfcvF%O3WGm)0lil<%NLAl3A2OW92ypwLQ_X@n2Z7 z0lgkm`QOu)8B(Q4!c6Ez97usWJ`(JAQhY>onnfmwSa#+=5t7Zc zXG1VKo|GEx@FFrZ4gevWI%@|vTV1F5FIB^Ob5%s2bT>R zj3B&U&1745K3=TuNGf6)u!vjHT~1E#mWrE4Nr?uGa;h4DKtzJQpDDSO{CZUJA50x; zqznYMNx6w!!>mC=LLpSs1SjRC`n2_63tW=L$EjUCd8tu)x#v3KQG~C(e755Dk9oNe3i}thF zrw_Y{JC*9&)whA#*M9Aj#R3QZ>OaErtzcJ=+L4dKC`Ol4KAZHy*>~m^?GLV>-MsD( z-tI;O<>aT?QtSOqjIKxPyE|5V|NBV zSdS||t)5h7BVPk3Mx;&6J=@>S*CHjyVD;Z5l5;I|;5Q9d2>1>hU4zRVXGyt*ElpkE^tH4oHefUTvowu z!Y&)WI#N^p<1;WS9lh1T0=7vf%wP^O*Qt5#%j*A`e_FG&kvLTOLRfp3I1WncYX^%D zIxi+&yVXg#&zHF`$1D0zH~7OxSb87K7nKrEwr~&OsWLBXI${O{Ez^`m)B4A7vO=!3 zqN$?MC+qEeSNIP$G4{B&?*;t}BAsKCA`UiPuQ(J-TPl?Cbc|)1{X|IA&orTGyz3mF zH-GYxP{t&7V6O`;@CawT$$EHr$YT=>R-0Z4?KIII+AN)}` zuFgVga7+{ss@pInZd}T>A=yL>oyQD)W?7!@`S6VM&xRGwhCL-qT{$pmJr^X(0A&30{X64PQ&y|Vx zW+odDUG2oPd z7FVynysoZVpXu-asqe6BpJ!U<Gg3{`>GM($ip2M(1&wR3RQR0!BUp_bg%WlL7~9{1GouHD?#{G;~W@mjrL zwkU}MlZh;R8Ots7&r0~wf?G)jwr{O&>-O82*hA4)-}IiqmX~bs6nchOKoS==)N4ix z_hfTKxj5pCn;Mrl`$wg8Y`GtkHd;ry_%5=!w6J?CwkbZ&kZ9^W%LBYvbkbqD=DGhR z_{0-+j03h)_Ugo8e++UlSxOReOWik*34ŊtfkM0MiHh8uLiGTv}C5RA&KLC=O6 z+1TeQn=!M9zZ%*~3nXm+O{bHE{$umRt=`0=6@~Gm^PHkeIFOZ`%G~ z|;;XV*DxFOAb*nk9H44o)1G3&e@d+BP2#Q14odWz$6t*~{X4N3cAa z@y%p<>RlJsI`i`qM$Bd)@)fCQuTnNh^zRz~J=KgJ&HGuihVS9f=6xoS@5l9HtaYCV zo9*bwt+scvx8(Mo(s|IX^v9VPh8P|FKV=I+3Huph#@|Hf6gFeSZib(*NNd{LFT}@Y z`XZRzw-k!qM4(+U_8Y^b-2%|utuKCH`aMZJjekayaus?`#|6x{x{UiA&01AX1xaQ) z_L6&|I?M2l8I1IDS2H$s1C_~E^`cKvS4L%e*IsE^_-X7oW_$St3-2oEn)`HXYI43| za_-*-cwe+E=Eb0!bTM9rtXc(d!nbzwkR4@R8#U++%l>u^i7u8{uL{7i zl}V}M)H*fTjYw=>(dlSpYh}-F&Q*t@G-a1Hiu|-6(r^4&`nLY<&{^$mc;bs^PYU6q z5a)|~mwDZnf%w7_gp|ysFvI^i(>*rPV&C3l&?Jx_xmtzB%fa7RcAw>W&+(B~?_S%ec|JDS=i9)RQv2tZ!yK7yul=$L zjxkFM(ctg>6eo^90nuD{&bj`0DxLhD{U26lX_)qUF!05I)4H=M@K>w~yxAH^rdy5! z)on)JTEOxE5!q=q-Z+IlCXLjsKj>85zqOa*<`n=F%b!GdFX zl;yJ-o5X6?M@|Kv#9#$Ih%Wjz4B6QYh1=k>2#`3!z{O>NSc0UDWC~U8b+9D`jcJfO z0nw#DcD#4PeqZG-&p?ZLs&V&tYiui+TZ%=B?NPOiU@t(l>>ot^rq?zrF56H{rH zUFaIk3K+V-PdX_f$6W)wnl+xnu8PQ$1Xg~ya`-}+@4=BwjZ9#7P}qqG_GN=98o)y zTC4#!jKF8`O#PNLSLCIMRpT;+xv|q4&$>kk_uboEbTQoikJ);4Ezi(40-UALUpub6 z@g&t&j*^%ac7aZUJwrqpu<9y>I!_#66Ro4VG!vc4h_Jbk1DRX*O_%-Jp3kc?bwFjN zACf;FN6#D!?COxV+=zA&86sv7t#Bbrf9Sk&8ZIf8gj&>XF^E4Vm?7SL%e0Y)S+?^@ zf!`rxW9J>^79{Mztf3?vwZ+er=aKU@lN<&_lSni=^Xn~*bX69<N0@$Ow5tK;vIm}GI4FGrLMojnc-BLecAE2u-0Pw5qlHnlhbqxC`{x#Z8Ts;R$qpNjJy2n zEs?!r68Ehnsn}3;R_^@f07A?Iz~N&S11)tAK9{e!?PU%wL27W$4MftLTTkmdd@8v~ z-L&pxAeK)>r%Vz4Y7*^MA*;zC6jwykjJV|A_Q}MORz{|D;_a5*FS|UFU~LM^<*C4W zUIAD_L5D@>!E(O3Cj-cir(9sQbW*lK>EMp%(22m%(Y;6}DF5S*Bd=SMS%}yEvhxQB zFAWAQTD`P*mCwr&wofmjn-+0Qo7v$cwh&)Q@`l|*}u43 z<+3^ViVWmk*^Z~x5Lbz2#76uV#sT8VYg(AuYal$HXJ4U?#loH z=l|0`_%Gw%CaKipV&kSLkWjnC=khU>WSJ~-qHKa(vYb#6SF&n(QEO|chb#epyY5A9 zy4d151d*$vN2N=l_FUMMd_2!)RNC~djM)b}N>4dDdoYCuIVhUv57U=H&pP&1Gsni^ zVu-WB=WK?C=@SefP=k2x){}n*}-1+oLOE=^<(eV2{u>gF&Mn-f>)*+F4))peu77fDPV~k zsMLyaT;{ja$l~`&mQ-?R)N*$)Mk3M0+y!X65^PffbsHygD|SJYjx3)h%l z_c!v=Yc5;GopzD%0^BSw{FKsV1bXy|1_RNn6e_TuJyD=Ect-gDkh&qO|)}J`|h2W{EE+8GBSOiSG^=8B_qbh?A!pI$#*B0(+q@WfYe;E zQN3|;a`GZnyX3k0nL>d&ZjR=QPWW>N!PgR|rc1d9E@SXTLfy{T zN1w)~HBSZVvf3t~+@Zt>^#x8i=jJtn8m@W3`-dg$XFk3ioTSF~Q3pk8bNc_VqW()P zMURWnpH?`L9e-D*Nk+jQ;Rhf98FTp>6BcgI#ADaoWVx(6Zp>ue)+L$F3YY6S8aHSm z2A(%=??$KB>v+L^JW;ta!*_Sm=rQmbE<@@NUyHt*XX%~|4Ya0;kJwuCIBVP9j9_YK zI&A+cc>AQMmc}oUz7;=nt=4r}If7?nW7DdAeMCOb+zf_*97G;GFWGvShC8HR%+r9u zqU=_q!trw#5jZ+55-B|0dPzCNi{D!oo?zX87i1O*nx1JOc~#4Y*+=)SDTQp_P_KU@ z{9{LXOz;gzf=`dLuHEGu#Aq=*N7n~4@+)g=-h23Z8%RcA-iQhFP0lVbT6}^mKt#(EBu`R|t{KVzFFL0Fg}lYLrB|mbIbX%M0tQYykE6#@&a> zV%7$q>Juu3M!Y>NFFr&V8#3^%FYIU4!f6~=$oP(=?f@#1_%*%F+M?;Rhu(r;g`T@flZ|D6UVY2zOZ4+E1vZA1 z`qk~7upBc+e9szcUixD6H9VC?N*k@WFbS%)wy$tHW1rwOa`j``6-R9FMkbhWs(-t< z+%0RMu1SA$95UuXKA(YgdKUA5jfgy{dp6!{LSP?D=_S|r<0bu)pqzLzSf#zbc;A7- z@@t3QP0plh^}wq6gEJOn`r42Od9tJRNt2ZBkm>nHY#Em2&#d%bzS=}R+c9vm;fXY- zsk#z~^0oe3Y<6n7wa8zCG-~FjmI`MaA|^9oUg;_|Cu1H=Y6$7zKtNL#{G7zgMa_JV z#~q4}qb|epBDIDTTfO00B|NO%(y=pqqp~mF^e|dsRJm}eQ(*VZb=(ehr_IP<8sBXB zsORMtosA{u(R#9)t>1?Pgb7PDxtcFUncQ}p{wPe%`=nK0zQ9)Q7(V!u6{komFT54b_snM!+V;lx}A7Y`Pf5t1Jx=-6In*gKk z2?7&N}RQPealu$+#P zYtWqA#0|ZCZ%U^=y47bAz{9+^EhBk$POl8{8f*6aj5{$hZLG1}qccW36F)E9^*gqj zX8CKv9Rt?&eOOwxf7q(k(8LVhx2KWX2Yu`hcB9*xnC+d;PLzD?_KKq?<`?UJ88EdF zQhNe+D$QGY4mMW95A`XThiF_Y|K^W|9{aJcCnJ2<*EFjvt=PvXfRVD3tH)v(&u-ZX zNY13-o!;7hfaXKFIl9iIXvle0J>RN#F~lo|J?br=aJ%s9sbWit=p>j>6-+KaY#6zQ zJCP#b(H0Qy$>2nx0%z*k@a>&@jlcBWEGy)AW>Iaso!lymIrI124IcuC zyf6D)6+Z}&*fEvpGc%MtL9L@QSyT5YMgTHFtnNkM8IRard^2$G*>$pEcypvpD$IP1 zET+pPrLv6}{5qKEVAha(H!r>pMN~wP-dB5Jox5tNtz<<&W@m@;QE}Wv7#7NerJym0 zSNA2-l2H@LSe{!y9C`-xHlZE`fv6e$l5=CT;uW=;??=Lo}YrSL?I;BNkH zSaUyW((#^Cav9n_a@rn^N-GG+T)hSkB5lS-0^94;jw`BgJfB+C1uhEcw68*e-sb^G z)}*%C^ov@J(wYL*sBjkg2s|H`!T+Mv$y9a9hlnP%sceKiyrW+x#1dPl8Y%AxGMb&B zZ0P}*=TH!)7W9)2cg^a-b9a+ugAPIhHu|C&pm`N>LUY71V)pq*y-A7{$vM^e?(S}k z6f}c=z|F>Ui-?SY>@V)=k7PbhLzA7s`DS9GUD~u=Vc#t%T7C1%P~TLE0m64Y)3Cno z#U{6-?2d=rOW7(bVyx>995r@UuDFX{hIv!@Gx3W`D*(hXt5UWhK#B3ijn~rWXjX8dfk>U)Yc577~UPSx? zk{Re)d`2H*6R(F<+<)EP4>k4|!>P!)eXbtzj+l){N3u&>f|lXhJ z-KDh{l+Np4uUb*0XH}nK6a#c@mqu7jjCTIYOMd(KV4@1xMy* zL_0C6^~o_#&a-4zC15N{HiM&wG=V%M2>p+mbKm-2AJR7SE%~BR6dTgBpO&v4_Tld1 zKJrjk%cB&l;>_)BunH&&Fskb^9}pvR&NX&E2UZ6UP26@LQ6jWE2cLZQXy+hdmj7_B z)bvLfX339+jd*L4;=otq&_1T3---UdbL)E>*iKgzW+cgi_-CiEGlpsn7n6%uOU<}s zE*dwNnS+cWtc|r50qN^TrM6^$ zpt|=+3YZNlwXPW;A(@|4AfS4vdl@Xup{u8x3)jg=I(fd~;Nk+v`GO>b2xkWh*hWwd z4tiEXw(s1H^>~OiNDq!+p?WmC%kl)34&4^5)ao`A6=hiUWbuEWI->$c;w{5{)LRzL z=*OnxXm+4Sn8}%>2BT;($v{o)^s^I&I)j+iFO?{&57lk_h=A`)@4h%P@}hiv{$uvPC* zuuu9r8HGKYt%UmQ>YAY{mq)UKYL7^A&yIfCEZcYgllxwm8|kZCg^Qz$Gsa?(pyWT% zY%sD)QP*C--)^Iz;oCmE(Hj&BxN@)1?qcm%s`KtH@nUw2HSR%wks&#D=cgyQ&yXTv zcmE{}7~6%5W(^E!ea)ogkJoh^a`Pn&89xoCMT{+?^JR~M8$}8rT^1qb+b^XmmCIA8 zDhnny(z-V4L$KESk}Z3;MB12!zcax2 zViSkhUY7@s5Tv?APWGe+*^|DETBewxy4@hp;<&ybkLI{2xuDQ$7z!W^(F*1AoI9)< z&PTSm8e&NV0)76LZ>%-H{JZ+!q1^sER_BJ4G`q~s;CU`rtOamZ3_~jz_oC^ePr&(0G z{hT}wb8v_;n}feI&``^X`cFh)kl||HCl$twluZvj+Z?c}(`!K)mt-W1aowkn164 zva?iqJ*;{??B07W^SL2FX1{tEKyPI6Q=JAf?^~j)KobT|lU;SJAcar-i-Py-8X8CU zxtyO68&PP{>$QeE8w6Mfzycl43k7s@((D>K;-q%3R7(v$j6ouBzW{3~(@1fj?$v2! zvg@v*TjV`=aDYDd#bQ9Xd30CDT(f2wBHHXIon_9Kx^g(F8l4%W{CVd7lWxoyUPiLl z^ANI|#R1T*0gL{3a`to4=D38d%*&!in{d|?aua9`VtViiW4tNn*)K2wyo_0^k>j$e zl*=3=@hs#%j&X-}UT08wgH0lqfN%!!74A;(JSth+9aDR0Oh~*!Pt~Bo&jIJM$&@~1 ztG9Bc(@;p-V21s_mj5i8hL*3@bOMHu`aJ9;cZ{%qcrye(zYcINq^n3C{*P&^vj0pE z*WVacu0Usfr~9Rw%bYiImix<2M`u#Gkx2#p>j)|Qd>%(j&sy*AFNnnd_=T(YOq&01 zyCEM3hWLI5Bz4h7X}mx8ZP$5bQLezRZM>yyyD4$Ji=DqLV9#9qjrw0U3TH zgqDQSPV{a>D&Ri2jDJ1Hk?dr#r|*Nn`IiH#*F=+>3)lw*)aTW@L}~7Ka_kk}EE$r?D`@^YMh%>Y%Yq#GleB2&bP7GC*-#(3>w$hf{!qNK*o3%3p zw8A9(PWwN?xBpjO^Z(X}xD;Bpx)EPe3iPhMr>z$bx_{BQE&$Gx>NKsl*~wb zvC1+t=1m|pHQ=wx9iHo}W>?|3-Rs9gb$)j|xZ4cX?QC4r5Y^1A@V~^;++Q8z!HrBe z%wUEfq&93NkJ;1``k2Cu=eLW2CbLoZEu8Xl=P@J`kY(Bt zeHip9G0%2TSwhY=K;Mu-VI&ifcEOUJh>&mCg(V&~8L4pOn;j~H22NHldpZ-fPjTD*QU zUgnDfHt!^%f}f$cYvk~~z9r9hK6C;gF%49+X#c5tCz?`UA$pgucfMq$k#zDCbXlc& z;&566WOvj|n*h)~_`@15NV9T|%~ka;JzLhw*Kr)1EB04f@uaucSY0`A(X;cy|B#wA z1)=resAsXaa55&4X?jPu0&3}QYEkunbBfR?JL~9z?WnvOc-zLci=Wm)V_LWMOnPfi z%0%CR0gZPTDYUnEj(h4xA%PS4yvPv~+_|;|M|R(7_ka)|4eymb@N&2AlRBMg3^=Ik z19vUmX&1YkX}3W3?3^2J65_`GNXllGFR7?AZ8FxZ%+V0jO7Q#idnuoE4GU9$SQ;evKSS@&h z@IjMxEZKD}tJzfx<`+P*r<9#yKMcoj?HufVCxrIB3X$+=~1*El+#LC54Ezk;Nw)TS3RHXN^EIR{sF0Y z{fj4ss~^R;Q^QEOw<9$`sUO*$u_EI)e#>-Od!WJ7@f;G2!HmaH_vqadQd&N_o#Eu- zvZ?uPri@0!xl|@dMTf?EBSm09oLj`@zPff5L%j4%)?FH)((_Zl!7Kc~ris#rXH^=| zgqEt4F5kd`y61kQ<#AH4v=sl`-G`_1O*qDF8@Bq-rmmBC_v7QE;GpxC_?KmU`?A%i zK$7cQU;W)kp*Dp#QMFe`(9_aHH|b-avQ2|mTj1rxHeUE16wh32)~$r$y_<;^7DYwL zdnZ1l)jVCE4Ioj4is6XLMU0sfwlB1uI*sAA?TZ8AVvWN#R+aB$=gfE3QE`ee`v2~83JhmFIl$RMaG6o{pR`e z+wx=NLCru+`j2mhEH|mE^AS6suE9MN)}Ok#^(*eLJ7Z%vI#{c`t{jopC*X-cKS9~g zE9Y0|6*kPdUJ%dblYoK8=WRMcx2Z%G?FTnCi62d^Gg{G3Z1x|QGwg%RhFDgbcC?K0%j6>Ui_PGL^bG#uBgC zr&7(U#?C$CA`>J@z$$vI%e~QsTfkAhG-pf^!-pia92-6pkEg0a8FF5Pi~Lw4661F~GJj$%n1Cb-zw%z!J)F{fGLK4n1<=LWBr*Dm4Y0I2Q~ zqsVGG*E%C4o9}Y=3n+O~Y<;EiM^R)1_~!~G>eY=jzXf+cAcn`v$A=6U?=|*LMng9+ z{#wkfja#?SF0GLpH3hV^B&u7TR4y+s)1Yv14EdVxtP+j&D+Y;Y_XMi7uv1TIK09>b z&};$o4Y(~ge`1sVqAG!$yq3cYSfAlB@9R0~=Nkl<%GZHG47iA}lc#CsYk1 zk8hj{+)1!pxT+4!%X4&g?aL{1CpWLZ{I}9~b+>+NNeNxQ?_)Y_*OfGuyqH?zxlbE= zgxmac6-KYmK(}YZMHEGLx2QZAIU#hl$d6rDCBHdvs9by{YR~s}XRI$9{oUgUaOcR| z7BT&SS0t)|^Rjq%+1i($7ks{KC#_Lg0@7auET~bJ+Ns zc8j~cOO@9*i_U;-zf4Zr^NI>3`l#mD?>1?(&j+kUGx}S0rEUh5sL!T^_=}Zl6cx=a zEl+{FkG$Z|C|bAbB*<&mMiCYmk77giAEb2ax2Y%BkDN+%zE!HfMBGTcK=3RAJMP3i z8_|YfUMeao?nV*jq-qZaMw!m*Lp#!y8l}pe9wF<8vBd*0v+ zsKB4Yw}z9ysAbm8{N%OXSEbw-lT5sQGrW_eJv6YtI;o!S*b$B{;J%@pzjwcuMyDK` z^?6NdD7WZZ(Yx-Z`o&m!E#HXJQh;ocJBZst+?Q|8j0Hi3vMjjs5{STAZdK5gJ&0JJ+Y zHQwTVH<_e&j9jUpjUA%WnmgT8vLTAuoO9LPihBKHae^ZTE5jwZvN-FDChIe`--MkB z53Hev!o1n85%|+Yy*4;!0J@ie-n~K`&+vG3^N3bs_e!VtH7H^v`O`^__~JkfqU8*{ z;Xf@!*5Kv$wGELq!6)*5imkD$CC2&D17GYH(pTdxu|^A)3XBIb4P%P*(y`C7o4U=u zgUw83I;%nu<7+#R)@UCs&_01=l?EkbkHMGY#HH zhSuv}KpexO>}|pT>W`13^e}Vy{++E)WA-1|4bObiR^`=r+6tD+UxX)xPzKqtq!4PD z#wt^y_sDCPqej{@*im$*w@L1=MU$|FNWHOdEhk<_v|rK?F7qI8iwn@582dy=3J25Jvy5G4v(bU2~n2}p1r9h zcrW-RI)Wn>`+$Rd&b_w!&ED@B8HnZ;vE&YyvX)UV>GkBglIaNJEI^2|$1@n?u<>BX{I#ueWu z4J==0ARL!{8~(?C$QLC&->|MP)wthICdKbb zybuv)^CA^r5FuZV=3-9k`C?LCetyvi=n`KV#P)TH?`phish?{7Y1zgUX0v1}u5&n< zEi6NtI!F~?N=jALf%0mM&By(%9^E;^Rhmvm_V&5@H2IA8ZSL;_9d`nwD^%Edc%F7$ z&}KBTFlMFxm{#tTazLqP0VzQ4;Gkj_mMl+JnUl)$GV;Y@o515WZ4=3gfJhit$=9!N z`AZA0KvrpvIxO`&mw)WO?c=M=%dr;f@=p4h$ zbn3mK-VIy}^hdHoHTTZ0e&GaC?xP4Akry;kQ$<^4 z5fs8exNW77Y%Di#E=tL=fRJRAts2C3J698*H-tp{+^7&gs*AO&KfdzjlsP`w| zbV%vuk7RbvK4BE21A+AOE%z|rm})LETF;;(a-b_+QnMUS#<-OyPD~`d&Eib8mE_fs|AH2FquewdYK-2F$ zIE`Z7Acis7F0KOFg@qASTRG~jU$jvoqiUMn+FY*$U7Ie+xr!e1%xg_phuzv*R_~CIGoy>uT-qoEgGCsqim*@oFYN zlxC9mR@VzU7lg;ASnfH{n6q+vdkn?DOdUy>MqiRDL{z;KS=2`km(A>j^D zWMbR=^$3{?^eD$UwZ~|}7udpq;---^Qio4R&_S2lK|qCtXC7`5t&0gK2$lvj;B~ac zGZWlTH}hliUOcX&KNq~eR(K!9tgF<%z39LVV=ARGm*I%=>np{e;pe_45c&!spzR0y zyCY(`mc_&c60^3c7-``s<`cx6<^6Z_nH`Opls3WltK;Md^_n??K=7xD3expFXdZ(1 zF#!1y{Npm|xKxls0{0VlO`F2f%Aoh13ZUM?l%D{x-p~#CzJMv9#c>py&cS)otbs9F zo0M;&lI>2Zpwq=D&^&*tQQG^yf|0J%XNEQKfk&xjv-R+I-LS^Om#=&gdB-y;k)VY69 z$)FRC8R)Km;t_*t_Jz1yjH9X`Q5iadNi6f{0dl|V6c$b!x1!Hn2SrIQ2R`66LzU<% zZtp9SR%ba)C9~UJuez+hj7suv>YW8GnaT*C@JUIIiSU5;w5dYFe_HZQI+LJ! zPM)_3^f=reFD+PGTZ0;q;OrnUZ1jn3&KlmoOqutP92uQpxXdrFw|&Pd@?aL_im#R( zqOIRjjKZ0qVSpBreP_roo+o1kO)H2W_{bI!}XQSw`{9~s6q?%L#VZGr$ z>oq(U|2iRfVUDIR1}}N?>FGO2z-v{*5bKAsgAKZ@8$6s%Toye4S7~n<*5(tv4ML$% z3WXMmd$Cg7EkG&ml;RdB?(Pl+3KS^rPH~q|EVx^73GNnL0tvAB{df1;_x-RR_S#SR z@FerhoS8W}bME^-BWTBeV|bNjE)#?cEscJ)n>WPs3~Qk!eI@(hgK21b{2yAI{x}!y zjLD1``HvMbOIEfkX`iRwO5^%Lw3l0cGBmo2_QW}=Z!YQp2a}C|qyT#fr{iyb?re3L z;(jbT{JEL;fz7qXiMVxDrQJlu)!n`q*}}?T{JhCmG-?$|gMZ#65e_qTMGe)rt*atk zW77$ENgpBl-cMq@k|`ZJ?ouAkn`V8b;qWU?|0Gum)5F}au8xiZ@;v@HR9$xP4SGZD z5<D1jK&+XD9vClIy=(JWA_wlr;NmJtOAouqY=Y;{?|YH z@(hGccL`AbEh*^Cb8hHjw_Vt4*phShrkAro}3Yv9&00qr*uE#jduVnd@kqPhGW%~_-FhBO?6=x*Shm$jLG0t<4 zm%m4w7p%NjBST6pm(oapGD>}du&cDxdDbV=Q}!@iT8&-{$B+5d9S4(r65xB13(gkY zL2GS?vaXNeWg9-<79sbSNPWQAJ_XaE-LY_F@%`RwVzzT3FX2%_)uP~3Cxh)?wg$rF zx;?C*Wp82C={{3D??8|Jw z$5t~xgi{@@p5p7*eNEfwUFv9qdwW`0H{Y=-Qv}t*{Vgq-QZVB-6QABH6|?beQ(54| z2Acd&sT+r+MjQtbM~7AqnYb?7AiTGz4s5L z;eW)Qq9TD7P|R;%#;WmeFQ-Ify`#ZP9F92LoG&Xb#3Mq&4^Dj*ltm`x-?OFYRB4aQ zUm?_F)4e{2@6flROUwl0rXk4ibQ?X*??WwZGF&iqQcYl1cG9OlAlYj?w@`iD`?!jC z@Ip4b<0pAYV&X?$*u^TMsIB^BP}4RmeC{A21UW?-?=cPjbVX>xhhQjz-ap*cP%Vl7 z(Qa=^-u4A=2;ikG^?c3rv*K=}Th1r8^$gcld{Svb7nQTIB%?H1ml@HFA zl57rRtssI83}*o-0exXeaSQr^9RFY)!ziBq`UWZS3J%%$!G`Ng(E{mX<3N5~`+v1( z;BNC&GIX)OR90jjQRwLErYR5thyS=5x9Fd>vmeSKxPM0|6 zVZ~Gj=V5BP76dK_Eu(wAN6e~A7;Lz1VM+*#g=M*k zEDi|qh0_A$d_WF`-*igJ6UMH)E>MiF@?u_O*~d{LGA3V12R4fJl=3O*lFPQwmzZ4I zu9SBbddjBQYJ`4lw0rsw784JiPIRnGV`NJ9Yu!uv=wX2|NOSJbEeFj=VK}hPOetY2 z;HAQJ_ITq>o;p9zeF9uaqCVGLtFW$k!IMnr4`}?h5ahg7Fdso0-R`|@rB)qFSsZT82;`WH}9 zr%Nz4N6QX=fJlG3)yHPL8)HzEV-x5brjRiuf+TV;c@VEPkc|{EOaps?$@|k<*cjxl zOwxg(75rUTMxR>7)lEIkp+Fqmg-PnOq>96mHIL@i7hcK{GXnszLUv@^AcvyRgQ;;( z8D0#4Z4|3}$Cx6eFcm=$+TVDr1dc2Kd_UlHKX$n9)Y#<)9q;`)fDHZZ;ZBon1I`w| zxmzj;AU~bT-zFQ4eBe1GqQ5I1s>hEWj$0Kitosdf850hFb~o*q`kmLwTo4CK;I2@^ z>(D=$vJ#1i|K$3~0?rLi;VOh9Gmz&$6gfJsDbeaAEN{$IuIGqpD}+Yb!~C zsv)BCMRj|9n(S>S^>?GLcMzz@zUJrem$ENt_9*_4ikW#giL2^r7c(iBMN4Q0$_q|^ z&nD##Tq%3`F&j?p^*oMugWkc=q^GR?f*E-(6j4ry!C@F9vwG7&S%=5sU3zR`?f;T< ztWvT(L@KkGK!&NcG-&#Ogpu-9R`M9kEYAq^73mk;*Y#?`o2a@nK#=9fQ`7w%xR#p< zr7;68=cvvHS`*S^^KsZYA5%!;PQ|d|lUL3TVZZ|7Fc*BGj`fo&HV1;OB*&3A$|t;M;XceeC-0yk}Qhh*)S}TJ8rv7R|%HuRFml zC-f={VkD+BjdgWvfzOXf;cffXy3z|U^Dc|VP9ek#J;yL_bpp0Cm7XmK6s`77a$WSi znW*s^O9ci%?3;swQO`mmC99&>X)|o)H+VYl+nhhahGHz` z7xJKlz5hYVnVju1_11!~uu$10L%Yw+cvnuvg<*EMQt=$yp{f4ME?|E98UGQ1zeYZ% zEwVsN_jn4MH)!_Fbo2310J{yFyIQl=E?B( z6=UUL!+Xjg-JK!qdnMubomq{6(eCZ9ePX9H3l%r~86x)*nE?UmFQ_MW`dTO|kO(fM zyW09knXlAYV$OAu$S(S#nCruJw1hEd_$*yivDJ@j!d#lUqcUy8$|O>ZW$?T(ZN9u{ zl1$Y(s_;*{-b~T7-$fz=TGVDkj9b`nK}VM|NWDgXhYEH_7cxP6m)tlzTzLwwwNZzhdzd8+i;>MdtKFwWU9Be8*$eZ0qM^Cl<|oR#=0RL+U_?k;F@+O!rH#_dFO| z7I5_UKmPQy3p4evcciFNi@c;jkiK|r(;6bYRG+ezUuiVxE}1?<)uQZTH0#2C@EA+A zAfBT!yt=N->$;Ho5zE`yUf*c>`gL1_E@;YgI1b6&;GFlAZ^=~C5>>*?Y9cm*7M z+jsD0dk5ZeNw=PAwE6HQ>~nB})YWh$Yk(#XSky#5(E;-vq}9XlWoe@O%6>1^ZSCFn zM-CD8PD(l+6qh-{zq^vu?X0hT>?6YSG(rZ|*%FbJxMh<+qqNbjQbiqVw-d zy_DL)M0ZY>`G9Xo8n{1}ekLA(Sj@#r?=BD3+s=#qzeY^)dc|ZI=ELnyV+}CDz`l2b zjv_!f?&*5<&vTCc$NC(JN@7pY%b(wJ~G?)2pUU(gJ zi&Ecs_WAopF!gAEu;;tYeFdit0pIyTH1U!U({GesgBwG%apL0I)hPa0*H1=mEV5^P zgxUrYl>cN`4=^zwir$p^gM@4Ndw?m^XhwMSFA1!00F($_6%>DNlr<|)VMo|OCjvV~ z{FM(nAk+x69ruBkTv}T9i>R7Kk$OOFKPfCT0w$GKs6qRPKOs4$*}i$Nx`{WG}0=s z?fQvAagIpxQKof5i%j}$y67{;^+9NW-|pOK8r1;Bgh?a~0QAj`##S(LvN##a4EW6} z^sQ9%eSth!v0zF_9u+T#SNJCB2A%3vdLGs+6u2Wfr!Co!Q_Arl58PS!bIS5E<{Od!+9`xEE7h6ch{pUz9Qb?@WvTwDtcV$r=Cm zPimKua+~ifHgW9~tE_;x_^P8%VWW*)>~NUHJK{eRAzn0WUfAC)tYstsoN->dZan}T zQ&u^_WQ#Ef+SRL5@3^uP4skqIZn3>=7~YJ|RrGK58Fg>tcnPpZtgzGo5gVfg@&UvP z@9W1@$tF>?M&o8E3#8$$i@UF@G9h4UP^`jvQmlN0>h)o80j3{qHt2=SxXYUTsyAa* zyW0}K3eO)L=mYrbhP_Hn9Iv2@dUkAzS7O!diZ)Bp@-@lj}dN3T$HS+3aQrUd_mxwOw2`r>}!@7zN6**@cMN`v!PLW(M9a zW8PoR%XXM|>`SPp9#n0^R*u>nrAcKSs`3D1+e8H*(5EEX; z{JYmoOYh1Hcl6)k{e9sr9S%z9P3!k#IJ}{DC*K=krE~Wv}80@93oHMetI**sTwgm4k5xXhN;3&r4 z=nZ9xEJlg~ypOS=%{weeVOG~Rs=xXk>UPhmh|L#QO80JfZ1_J$(IX@?Z`m)D8<*=uY6kw$2tq1hh$ah>O39Dv!& znpFV^yGUfiAg{f?(kI3LibFKqb~Q4UvrYckC{LF&XkzJpN1S!gr%TtqVB~AxFmu>0 zh5yhY+zmJ0grAp)KOVYD$Juv^PRK(*Mi63(hHBXXv$(I#;tx}CMk_X+&tdXnWkg`r ztmP%|*r8*vo$n$DIuDii-%KP(98v$TB83svdA3#J*3GbEuM&wzRpPHVm`sAsLnJko z#x)0&6z%3ftqXu3)>=erRgU=k)mxo^6Em{bVx;7mw(VX1;|qSurp>Dzw9MUYRteW| z!R2ftMKN5&5q#|q>?%dQ-wRT0HU!f;s8hG>^kvmohG25Gn5V~KUf#MVfOY?=cQiIP z->uxl#H{}&6b7-hGYku+{x*0UkY`l$92rXFBRMno2RT^>6Jf4tU(2$VtUHiT9tD`D zLX|X)s#e_gW+z5D+RxkLJ`#et;OB3Z3z+A+jn^JRMA%77u28hl%+s?mom8%d3W<+g zc3)E~Z`-%f>y>xpKFpS|pWfU8S)yULPfrphO9s!0jb?a%hKO{z2avEeb@W;yGJirc z`q92qI_mM1oaeqQ)P9ydt9bAnN%DYe)^h}ME7s#_t7pNppePTF;(mCyhpK ztL_UXVQ1vcs@n=o(nZu?)L>3g0+Lm7_zu1OlI)pl=6i&m96QVwr3OKLDS02|6GCJ7@mdB~Dd;U}YKa&ka{R zVo?Sa?QaB2_nF~ZOd3kB1t?7K7v7&c$>6ii=`QB{W>N|$p0a9JO3-!v$eFyHPez?o zAQnbpn@oQfbaEEhJ5?wvPD-~Lr1%G0;KO4J0TcMT0Gr*<^3S?B|Ce)KbwZyVi{NT4 z7J*#R2KXg5yWky~LMO+?|7LEnv0uB&`gbSWS#uGYEad$EWADO!2iD(Hif{en3Lzt)$R}!_s%gKOElU*lzUU_o zRMCw(uB?EaZzvbExc%9@oKKx!&(G&@Z&wjgY#{MXP@x`=7(7p~D(me$1H-`&?N<|R zMj${=(nyv_uSPLmi z;>JO9SLxAy8$3G~(AspfKOzU*y7^z1UwhQA2zy1$edaj8USg|`ZFXNBV~qWLUjrkR zd-3J+0vI#}^$64e*_;k)joWrt-ZH@Z1m92UVr+Jn*_!g<1^dR zIldqm)SF=pE{nLK7gk6gLUY?G*{P905Sg9q|MFCn-9y(gEnVg#6`}t@=g7Zan;~8G_eT5SmMi0}e z)x%o-(Po5!#99}FanzmWLW!c`F|PAsFIFVtf6mzm6Uu$*+)GeoUy6TdUMy>0*xS#& z{bkvdkRZx5Qg;2DW_RFB+tJ<;%%k@x?&MMPm?*H~gqE}C(h?!4ZHN4)_L=!3UJ)xf z7)X-Ee@No*qI>b`Q?8O)m;XgHu-(9+vO^so-ieE^BUFrX$4vAx*87X!VrpuuzmgGF zon}?FA>*)-Xt=PM>NMBRC)yUHb+5~lGV0E2*aaj>!0C-ga-~-)Zp_u;41l2$;6$?J zlfcz#_Qlz)COWsVPcJ93rv2FP%qM5X2|*|?mrBn~t@k~|mkwWuepr0*)Q$SY?T%XB z6H6thkzyyhX?dC1`$GxcPi$sMaQ(zq@KJV$!(GXLw7gt}k>n&p!Jn{mg;f&4$g#A9 z-Vi~Yi){B_{GF>Zc7L7ZVY6PXkGBwNpC$9u$8!3DGkJ4%QUDc6ddlRG;y8P}eGMBi z4?dV(>FM)=?TtuLKw5FC6F=V1K=Xx*jT@%Z_U(O+PQ)c#dO0~w(pf&c&5izo+Gg2m zwx*b+{@2u=vTd&^ziKV+hv~WSW&|d7@-G!UxsZ5izcY5-e(|1RV_N7B$N25J zz$&A_TCkS=$jff;&Dz}&5CqKy_0g(i)u+Xe{DGPcSG&h!leO+|dYAx_aAjBDuuUXI ziuo^o$9azeeX)y3RRgbnLTyAr`{~_%!e`vkH&;@*O7iU$t4re3kah6{O;x>~g@?Zn zsouv6N49sG@MLVhjvo!)pZz+0|D8P#R8e>AYH9=h`EoMq6G2EKGYz(5LXMrzn zpeC?Pp+`9gp7_uG5z3EG_{RJ{S*5B~@Z{@bz#o%9khZghgskne^mU){d>f}T} ze?p4JA+Dj}l_My1BLsTdQj3P=I^REsN#J4R{|#7ZbT((BxdieMzsl7PjQBjsG+&;a zI?BTH#7}0Me!`>ja57o>3uFAJ|FrY6i*s8;=kxcLJP-I1*7JKM1-n$B~D!sB1x(mz_&pTm4ih6TbyXQQP{YtgH z+Jea=`{m?R``HK-8_zp|UD;t4N}(Oh4c{*!LpJLsJH+Dd=3&p3NLtzEi`3zO5SpLb zl>|fV6+=a)9>b(7!--7PKBwOM9okyXsq7WsT+kC*_2Rm) ziktRrpF}Cqv79_pK4_5Bf5qwsTF#W*pC{eYY1M^#@SdhW56yg0DQgicFh# zw?KAZ0)0ECU_3&>+VI40Jp6q)p3sW~~K!gvBWnQz^xQ(c1BK(SAq09U}OHD>s=YLa)Wq#xNP5vYTwzSh;igD%6QZmR_q z|M8FPL7h@ZW&Zl(re$#DlQ#SPX#4d}@RDOpPTw#|y0@2illx!%d7C&vZ@0vU-jry_ zB{++z+R+Y?omELbY&e-Fyjtc;13$;Cr|#6KfYHZI&}|++rC0Jfkfl91=}RR9z9bI* z!!b8fQ!PLy&as`wu9v#@u7SMkwbZau7EdJ5fSaqjt@GJ+8rGk#Q<1#qS>9-tX(36K z$-BItO>^_x0+quDptR^66Z5d6iC2Hu87{A%0hi+@KKl(VcT6FJU*AhR8BG|{iaLMM z%jeqVOOp5BM%o|{77=I#xMxFfKve+F}m)F2ei_Tx>d}UlV1Y z-w3AtpWQ95KXM#e3ydES=B~E6skX{}x(dmakczw3;_pMN-q8@Ym_M_zfGi=#h z*4ZPk_U==TCje9Bsw|R;Gb{P?t-z`l-ny^BvKr&0T5NU&%yOSBe^{!#%e5NMr(+)% z1EN}BpbE?XR*!WDBw2Jkt?xown-n@9_DIr2)I@&|GP1O>-G^6D)N?t4?0%o8FdXil zCNQWEd5_pum*U~+3@dxx&ym~@IC^$|fOzxVME!s$kIVQMD&6w9yOv+x8?AmD*Ch*I zd1Pd%i8h<_Z?X4oH&w`8R%{Sd&tKizx@~LEs&?Mc1%PE8 zhPJ#PCZn<&I?8vTKM7}3eYf-^s2Sccdp-}#-;O81p4E?(CUwrJDTWlPnC?O`_o}~q zQFK8xHfmnQ^^G>NJCF3L-ss(`hJNs*X_Hw;ZiPVFC`;3G_VbxV0TdXFPQV0!QKiE`%aOcoN}_j~pMd=30yomkHQLY9 zesS&aXVMVQM-?b*Ny9AVLGVhVhH+=bL`o)bx35jp?++Fm*(`Kcvn9*(566Npg0%-tNQp7ESJg6xcHtTQ|7g12*V zS=2guHo_P12!uQ?Kno<#ELy2kYYK9TB1C_U|3iFcDUCh6mKgv-`BnhAq#KIbO^ zR`GWW$TBCLPY-HO365e^uX4NZ7aLAGly+5xbg5b3*JlY2Lq)M&ZH~*?E;qS*;*5Tm zY_?munjx|@bluxGIEh5MevLca7uPa{4@epEYs2qV@r3O{_jW{n#@O5(v$=GBp2&0X zUKp{_b)3s(fO3up9qABWsP+!S(eB8)OJ(-Cyis_cxN?GOg0#ckeHhZARIkn3&4N|f zQL1wpSQDSkRI=r4=r5Ft^50KW$zAP}bU>is`cKq>O$oZ)hW9dm+9$e8#ZUCbts|?f zBE^}%GW9DaP*{0&;@)oWtC!C;4?R7(GTa)-uZ3ks>qe9%)}gzlVTgy;Js_;NE2^}P zNnAk z!z)xxCSW$^Q1fvMs3)b({@eRlBfNJbDpCc`1Z6 z2R8yb;AqvePccq+F({v&ZBc4i-}fef!e6BV_Bqo$xl{jeA!puBYyIqPx=E3UkBC(1 zcj;~$;Wc_VvLa*khfa5R2{DCuz=G5;6*!o2AwG0{ryp5SW8B@fQo#0-p$1L&F?FBP z&~`;-oj}O^&GQjmv@qMCca$@?rx$-fhpqtTS|qUX1IXcqr6Y3}H8hZDF+kaCB8PqX zfY=g`LO%ro*toRRydCeFjXsl`m%A$nxn*cJzA9Ok)Cua`;& zZ^vK0Y;fOd&f4$}M8*PwDib|#W-R|TV&(eG;ENM*-#5&%>&Eb`JF#sXi6VnVqowud zIAjtAliz6Be9KdifyIVZ-dt1dk{s_B?5oO1E8z9Lz@_)2wc>s*P6=V>MYQyBJ;QT5 zzSzDtQ$Ti-JYE&j$h1Rdgzau%TuPC9`Vh!jTg9WKec}DvIZ#$1h}fgrnF*I5VYS$%p9)psUNwMmBm=0fhoRYxxq{W5(%!cE{R4x@yRKAzyq+XwI@76zx-3Q20co)=MXHBA+Yup322jgH$DzkJ62N$6` zz)fPIAMf2ieMlcZJJ8ypln7gjO{3pDE}{@FR6;hj^?NAe4M3g;UoYQ~f=tYNycc^{ z1pPVAwCmF{#VkIdZ{DxkMZ&VKw3;1Rg#hgjQ1N1g6dXFLgW(t;BIQ$*=^THLNQI1C zkL8z~J`8;#(z2QW0S`jQzbacLmE8Comb>XQOac{i`p7sAITO-_0|{x2DYX|}X>mJd zU>U8Ovy@gZJVB17qT$i49r8}YE5HLFv6EvH5-A)lVpvbc)Cp~^#WqNBexKuc9u-ck z*#38I9PMhf?<~EK+r*oK&Wiq_&J523t1)x;k*;1&-?xZ&?S+9Z;}wcgTU*morJZ9{ zCfS*XM8a&O3-|b@`U!_^uAa&$eq3ri-ww7tOr1Q|+vs19EYMHo0up+8cpxkO5U-zc zTz}WJRtn7geZobv@2!uBpIGbR-fZ)yc+ZLN1}v?q5^ZK5?f*YRR}QguDW|(u&6LHr zcxjFZh0DR0DC_UD`vn5hf9pmpN>h#GSxgBc_E-qo@83%l*H7%Awp3lL@sxPh#G?aK zp9cl_GityOM->-^c!C0w6O(FSB@Z?c*Pq)d{=amgco&i5y&eYto$5zS2jhSoxi7#% z6{rl)e=&b+h}56O-93&X>QUGXNmk*(L zMpqM_?c1FIq_9hjz}wtQlhR@4uamXPEVp$#i}GZ^US*?5h&JwZ$St*rAzF37^Ic#d z^ezzj;f1W6Uc&G-OLnt92bnlW_*TWrKUHI+5Y*R16#Y0$m=nC$(C)?_9^uZ$>?_F? z^WD2vr9=X519Aieu2yfjE*VJ)dJ^e7xp^5&l1cw1p~+uG6t@{~{A2>mgLhN(!CU1O z)jogdvzb(_9hupwx{#}DphjiHI<}4gteEn*d?$&_h?zpwKI1mw^eU)w%MpnKK3_K= zVM$=%7caLDj9E=G+DRAg_T|TlusAyvkT; zKgn{P@NS_`gE9}QMMYzt$E?R978TWXT@kMY2mm{^nxWBObc?To{2p2ZJuUZ%b1##a zo;Q)D-`;6|pBL=WwIz`!&F!{!%Uw~}Oi$CYQ6s=*94F3D5v+80xFZbXnXznWCk&5DE z-3@Ljc1IBBJ(lXv+C?h-j$lHskbDHzs#FNy(Rw}kqftHd(}uBIt+m(KEE8Z8i`tGq zbDb6IFod{$<0W1oQUwwyo0gDTVXCJ^7|iNGZBrQ~Rmmb^UoO^ zysIw>N^HBT`JLN0mi@8_dOZgt2|C>;;_1YLG_4z^N{vOfoZh!Gn>R)&2OH7S`?jwG z{V(Q*$qPoAy|FP;)1=Rrz;7{cU7j1Gl%9 zy|ci1T)ZT#_06Qu?_c10hX>21@y#ysZt!N?&+g0#b|nYYcs8R}ic<>RJLMVQbgtR4 z4^#0Pa7sA;6jD>^_X>=w_-y3i^2s8*-10LQ0p)TkzM;+ANJ=cKNU^*Xn~Q(;WcJ%` zke6*v|Mq*2!3v9;!Q7j8*&#FvA|<3&J2Un@k}-wB^6h(}RBdm-@3PuW1Aotzb$>Oz z>1=DZ{HXS+vylNlC!J>0I%4AXYJ~1oBF_3G5;zv6;j8ht(gJXCDK^PPDWx_rDKW8I zb%FItpKq~#SbawZs=HYd-B=nk1GP53&fj&<<^Lanwn5t~QWMhb5tOZ#8VEayphNq% z(ln?0Uj&LC@b+YvHW5{SXwcei{?Kg<_0M1CNGSCYiUU7`^+!_Q>(a}-!6dq=s{z(X z@csd&*luI6zE7D;)JM_EVN#n>wfZ!*W~K_*LE?8JY>h+U@sn`EPXF3MkR$$*XDI_tQYs2W>loYE@qM_ z?@fGkljC6QMfuxth^}GO)ak0qZu}Qa7s3CtcuWGBlCfvNW(G4kH;Vi#G_LPx^6$f5 zyQ_VjA3&;-%X+K3P3vR)us#}XCSqZYh}~}$Ni)j6$pKY9Ip_VTc@kocn6_49w)r;X zJZWJ7?M}mxQ-~suKk>HYzeG5_2!9K%8-4Ngc<@2DW;DjxKmuU?FRy|8y@*i3vQAuipWdwx{bZmcyR!dS z_K(~ZN~{=`3Cs=kFP~kreTK5Eh@$i1=!d|MUY33qHNWuOIn6~=4eR+;ng@+LBJkq2 z&`lFOy%)hWK}?ak!d){C%`C8pkWBFSp78L@0%{JnaU`_MKdO4U{ELF+hYw@CjtL1~ zL=*jjF^Prz?`@v{8p}26`|f26Tl7=wZ({rkpfx9r)Bh%T`#C|x;qQ^^r~K*b%)Msk zR{P%i9)W5?Z(Pc9-V+0Thohr>fVyFJW=Do%bkojH$>+`(&DW;`C>=ay}n#srxM^|E*=I??$3T)IzWfFxU8 zvVJ7Aha$XKy-bEJ-7W0IUrsJc2#;c*iUdP)gQhK_Er%IR2VOy}CymY_Kj#>yaMm}B zFlU{HlA0i0ZB9F?6(#Y?GaZ-L+?%T6{lX{_mU3xEsg(nyi$^ zgI=n(tw!mdhtA*s!E=O<@;n=j7kVg1`d>6V!E$%N_^59r+W-K`J_R3xBvZh*JiOBy8x1S-x$Z~u(~%-JVq95NF35#OyB5I>cfkK z9GSXjh8Pb;lNz}YiE^8yoSJWs#81gZ0vBdV|9-IG|E;bw^4ZQ@O9v;F+ zv$!ajbRj$gX0){-5jB6>#=^E|Ly)P|^8=4pN~9N@l`}*(BzC$Zy!E;dLT>HmJsf*U zB4|drK@FVbSYxN5@|C zF1Pe$I?EugL#t^qjUFU>_hPb;dg0dlS%TYg?B&n!yxiT-d)+HeH+~{%e$^SKBi6MxE&SdChKaJy=|nOyw!GcB3z9RBbL>oIsVU z9#v1ycpnru#U4w?I9t$EAv$zz1OxCBK2`k6L5BfKhJeD)kF>c^a?rT#tDUXcmEXRU zwuf-I+PIP@%`l}r)v%cDW?EAg(g9a;UfYn~Zui6!saaB61x+U~ETNhAsNSk>mfp$_ zn3-;0@vbajUdSWCw?OY3*DdVzJl9%PysF-t8mjeI03F={EN0%RZc$$Ulife%-8e zF5!^D_AUL~@_0Snc7Z8R` zCcA*L^%d<8q;d1v@$A9$n6-klNxzMIi2}!VFa?V{#qP&D-Jm|J*+V5cIU>3Nar79j zr%h@Zbm?mpI033V4Cifei?E#qemudNtRtE_q^42yO-PVRj*(o>g=X7<&qG2gz1P*sE`rs#qh<>Lzia$%p|o$0m> zohrrCmUFG~^hmCxGJ1DBO}p5b!)YA&zpSwC)Q=L{-~UE5MWIr|s=l`l2*cPYaf)Zv z5k1n?1B-jDA+ES?v2|{dGr8i487wz0k3SAa_}v~sZ=BessGSz|dg8>Ei2Qw2X9}qj zS@~gAWIi2fG44w>Tu6>o8DVs<@Cjzhr+Do2jD2UQNf%~-ua~2Z=3_yRlRUGK8J7W^qS!lg+nnnjDP#g0v=>gvMTy*oRpMgsm~h> zHB-s5Oy|&3K2`aYwNNb>VO&f6=~#7pI?7N5c{p-b51~AS zph~LM+w9udP2`B&n+6N5uR!FsOjqjT;|z}_UigABJF=z+2YdH-Ci#t7RyzNn5ikqj zf;6HJ=M@&2{On2&5{RQEl_`6@(kGg(>O=>7z-K3Cz}qW;kwVpL&jw8#rn=E*#i+QQWeG&yhpx_22O4!gQn2j<`&JHVW)|iq{_b6vcW$F*nU_L0UTbt3e zWs`{s&d4;_DagrrUXxBXf2WMiqCfq}W{fg9NyOSjpHd4m!)cY956em$7n>SCo-O-E zZPej&)6w1X6*K8g)}a?`?j*>CZ-D<;Jrb(aoSJT^f$`=GryC8!D`9&4rZ)zPk=wzO zkB2%ScM3bX$S`(%U*oLfx@uobkzz$LxY}gT21}xagX$#9{s5B)vM9uU0OGtCZ+jt{ z^B=AIAxZW2iUSuKqTTLd+OQ?7pUL!Jg~IPJz+$oA?UH2#$Z;Lal=UP|7OpC(k|wJR zB(X6F;+kh*Y$!v6?h6Y}p>3lk70_`oew75(tWPr^MIRpug7PI5a_qA0tCR101!%zk z=r}>T&4@1bKbjn)*}`XKoACtA_7r5oWOe}k#w0akT{8d3&G%b2A|ylNkmV@}&nnXG zMCXGtb&@Zvi_`q)7%dK~tyj3zVu%aP3avBTP)j+?_iyMZE9_Z3i82qJ=8Mf@VtLT4|X{hhFW(Fb!0OjLY_965d5+`Js6(z?aE_Q z4PpbSWlzHUH`o;`uZtySJNF@%@JuI~M2?Dr?WklXm3b@C|6fzmBeBVeW%cjgv@&Xd zm(w<~tVfgQQvDiv6^g5lZZJm5Zn?_$8}xlO+qc8`WDB6F0oM1?$=2`5VF?t+-XKn9 zt;(?c>Jy>))V@AHE(4dSY8@lk7{b!XpCM~Ypc-yFw2%5ml(CKiQA=woh+u~deeT7B zFE1)KbBuGKAAGo{rKqiPhZ5_v$7afW0M;N$8L4o1YG7saoidB(iP!T>%ZXq9VmM(0 zuToKiF>BS+mFqYwdw^p{28*n_Rpj2BL^E~#h}vkgWSt6?WIq-n5=C9vlXHeP-4j#F zN6wVp?sys@;!RW$N|lSY&bw7it*Qeccnc8>#tSofPTknwkNu;F)qOwB4>thz;hfXj zzH46Ms`_(YX1Z8ih%KLmH_eIz&hmS@-xmNn_wRLV;&q6nxuO?t&Sn@Se1^GMs3w>DD95HWc2``<3RK>yLq?9PE@41Fe|zPPfOW*~oYf1DD|q>W{e*d~R}B z(TB^j46dK{_bD72b5X@No1dtLutR|F?Iay#MpOjxM37$F#pQUuih~shwoA==a%|b% zo7=n&)Fz=X#HUAN@Eu|f*lTcf8Gb3lKl>NMhHxmvr5O7$eIg@ow%R8@mphUmaP=X9 zdftc5j|Q?fc>8$yMW`R|`mTcs2fiPIJ&AhHMy`m-sLsAPqgD83#XN#b?J<4DgxpTD zZQQBcHOlU`A4z?Lvx^jEfpJDRYg*og%ad~Nu|Ew5TZ)|KhS{dRv1gpDIMyO|DSITL{aFp+YUmAC*!mdzQK7O>w?;Qn7`AU4he<{epAqoD(dxk8ll6QmGc% zAyzVjmL6p?otok6;u#_z!xeh#x3&@!#@qXh1D(%fGo4gP-w;txVB6l>Kt|Z_kx4A$ zlq8p5(QK(<%Dz#Z3OT^&tl+Q!L-tE(b5Vydg?UqvL|)2C&-3y2qNdOCXa+FyCH%+`E^9^B9(y zDCs)UXFt}jE)S=s+StLcp243))`Z$o>~$2DyDt=zwK`@E;`GgMU|5|U0#ZgwUK&tz zZ$yY$rQAM&__4H5o%?Gz6&ab2a2BE_dFC1H51)^idB*F-@+jc6#u7xs{I<`?6nL=} z-<$fztn+Jwoh!*Lyk~y&H24-<|5vamc4&>#{4iSu^ogJQx1YMLX`G__q43?n$Q%iT zxpgH%hvM`dJXm@c2{AkBG+2x`OLmDANMLXLww})+2$;O6n z>r6diUCo)jHDc@sodEOJCYue_oJ_wh$LFX0)SBh?d+J6Ls#@mOA{Pn;T@pX@dKCv< z`#TPGj0E8C=*H(i%UaoRWbp6{THd-sFwreq&j};j85$Z==Api82Y2fd{FZMvM(Zru zH-jb5`HEc~sy-e}$xXW4x0dtCA57LL91W(PpBNn6ou}G z5nR`nCg;05Hw(b$+;go-rX9>qBzNQ{Q&v`1oIKz_C=1&r|J^*5sPFAbwr^+C{)_wI zO8u|QYocu}1W0$&l_R1_DpjwhtPR!i>;3>ri;(l6_T)_o0ZCwe~ zpgtuYMB~}bmyBSC)SHj4IX!fC<8XdX|EZ}8+uVQMDSCf24B@vU5td^3^S=7M;dh~l zk(d8haMj#La!wH7x$Z=YfheulpXgPa(iu|`ol_7uKzl^ajZlj{1k{OKgUT)s>rJ)` z<#ly+kX|f5dg9q1?`(dB0ckt#O2oZ)*j%E-1Etq7dupG*eGFUkimOCk-Ea{8|MVRj z*>`#O&9&5rl%o}27Hw0zLAMsnGO-noSWJr%I%M9j1#vlghP{rnHi*j2Rz(m16Wk1~ z$@O^z<>yD^skQ&uR+wT)*{(D&E}Je-%{X?R2;$km;pL#nOQbD+2M3MXp1%1?lm+xKvGc{bO zvN_h1=7;N~@-2(O{_Oc=@u;OEqw{2D{Yxy(2tfiZrqpANxf^41k>XLZlirN~ zwN+*`v2#V{%b*t+M>yx@Ej~InBJ;(u6*h{7*#!EdJ;ytGHx#ZI85YP$d}h|Wp!@S2 z?F2){aI%RWuNqtKKqH%|F3Fu<(ZVoW`LUQ~9zWW6`;ZOC(aRKcGyE+5KX}l6TL<2)|4Hf+Yp?=P6&PV?6NF3H3*I7)zE#-4gqU5DA6cm)N@=_8S nmR+Py7+I+Q|3A{qH=ZALI}|S$OJ5-yN0FCSk*fY;9P)nvzQsq= literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/supplier-onboard.png b/erpnext/public/images/illustrations/supplier-onboard.png new file mode 100644 index 0000000000000000000000000000000000000000..30335f2b63be25d769c2104eddb7940389049536 GIT binary patch literal 17531 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 literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/supplier.png b/erpnext/public/images/illustrations/supplier.png deleted file mode 100644 index 87f7789600fdb38903b654b3f3785817badb5039..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3531 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 BlxP3| diff --git a/erpnext/public/images/illustrations/user.png b/erpnext/public/images/illustrations/user.png deleted file mode 100644 index 7dd7db210d481d3ad5d78277776102d327fe21a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7887 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 diff --git a/erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json similarity index 77% rename from erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json rename to erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json index a0bb6fe26df..f39fea48960 100644 --- a/erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json +++ b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json @@ -3,18 +3,18 @@ "app": "ERPNext", "creation": "2019-11-15 14:44:10.065014", "docstatus": 0, - "doctype": "Setup Wizard Slide", + "doctype": "Onboarding Slide", "domains": [], "help_links": [ { - "label": "Customers", + "label": "Learn More", "video_id": "zsrrVDk6VBs" } ], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/customer.png", + "image_src": "/assets/erpnext/images/illustrations/customers-onboard.png", "max_count": 3, - "modified": "2019-11-26 18:26:15.888794", + "modified": "2019-12-03 22:54:28.959549", "modified_by": "Administrator", "name": "Add A Few Customers", "owner": "Administrator", @@ -44,6 +44,5 @@ ], "slide_order": 40, "slide_title": "Add A Few Customers", - "slide_type": "Create", - "submit_method": "" + "slide_type": "Create" } \ No newline at end of file diff --git a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json new file mode 100644 index 00000000000..bf330d09de4 --- /dev/null +++ b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json @@ -0,0 +1,23 @@ +{ + "add_more_button": 0, + "app": "ERPNext", + "creation": "2019-12-04 19:21:39.995776", + "docstatus": 0, + "doctype": "Onboarding Slide", + "domains": [], + "help_links": [], + "idx": 0, + "image_src": "/assets/erpnext/images/illustrations/desk-onboard.png", + "is_completed": 0, + "max_count": 3, + "modified": "2019-12-04 19:21:39.995776", + "modified_by": "Administrator", + "name": "Welcome back to ERPNext!", + "owner": "Administrator", + "slide_desc": "

Let's continue where you left from!

", + "slide_fields": [], + "slide_module": "Setup", + "slide_order": 0, + "slide_title": "Welcome back to ERPNext!", + "slide_type": "Continue" +} \ No newline at end of file diff --git a/erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json similarity index 60% rename from erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json rename to erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json index 1da9dd44e2b..4ea69852afa 100644 --- a/erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json +++ b/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json @@ -3,20 +3,20 @@ "app": "ERPNext", "creation": "2019-11-26 17:01:26.671859", "docstatus": 0, - "doctype": "Setup Wizard Slide", + "doctype": "Onboarding Slide", "domains": [], "help_links": [], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/onboard.png", + "image_src": "/assets/erpnext/images/illustrations/desk-onboard.png", "max_count": 0, - "modified": "2019-11-26 17:17:29.813299", + "modified": "2019-12-03 22:49:12.871260", "modified_by": "Administrator", "name": "Welcome to ERPNext!", "owner": "Administrator", - "slide_desc": "Setting up an ERP can be overwhelming. But don't worry, we have got your back!
\nLet's setup your company.\nThis wizard will help you onboard to ERPNext in a short time!", + "slide_desc": "Setting up an ERP can be overwhelming. But don't worry, we have got your back!\nLet's setup your company.\nThis wizard will help you onboard to ERPNext in a short time!", "slide_fields": [], "slide_module": "Setup", - "slide_order": 10, + "slide_order": 1, "slide_title": "Welcome to ERPNext!", "slide_type": "Information" } \ No newline at end of file diff --git a/erpnext/stock/setup_wizard_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json b/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json similarity index 76% rename from erpnext/stock/setup_wizard_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json rename to erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json index c536f7b2cab..27a30627eec 100644 --- a/erpnext/stock/setup_wizard_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json +++ b/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json @@ -3,13 +3,13 @@ "app": "ERPNext", "creation": "2019-11-15 14:41:12.007359", "docstatus": 0, - "doctype": "Setup Wizard Slide", + "doctype": "Onboarding Slide", "domains": [], "help_links": [], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/product.png", + "image_src": "/assets/erpnext/images/illustrations/products-onboard.png", "max_count": 3, - "modified": "2019-11-26 18:26:35.305755", + "modified": "2019-12-03 22:54:07.558632", "modified_by": "Administrator", "name": "Add A Few Products You Buy Or Sell", "owner": "Administrator", @@ -26,15 +26,9 @@ }, { "align": "", - "fieldtype": "Column Break", - "reqd": 1 - }, - { - "align": "", - "fieldname": "uom", - "fieldtype": "Link", - "label": "UOM", - "options": "UOM", + "fieldname": "item_price", + "fieldtype": "Currency", + "label": "Item Price", "reqd": 1 }, { @@ -44,14 +38,14 @@ }, { "align": "", - "fieldname": "item_price", - "fieldtype": "Currency", - "label": "Item Price", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", "reqd": 1 } ], "slide_order": 30, "slide_title": "Add A Few Products You Buy Or Sell", - "slide_type": "Create", - "submit_method": "" + "slide_type": "Create" } \ No newline at end of file From 63018747954f7bf229f16a729bd4018bb144730d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 7 Dec 2019 13:20:37 +0530 Subject: [PATCH 46/76] fix: timsheet overlap error --- erpnext/projects/doctype/timesheet/timesheet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c4481c9aa0e..e90821689bd 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -188,7 +188,8 @@ class Timesheet(Document): }, as_dict=True) # check internal overlap for time_log in self.time_logs: - if not (time_log.from_time or time_log.to_time): continue + if not (time_log.from_time and time_log.to_time + and args.from_time and args.to_time): continue if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or From 502189913a5339bd43ea166457ab70b65bfc7bf3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 7 Dec 2019 14:10:11 +0530 Subject: [PATCH 47/76] fix: 'NoneType' object is not iterable --- .../manufacturing/doctype/production_plan/production_plan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 25c385fb1e5..8876253e8e6 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -615,6 +615,9 @@ def get_items_for_material_requests(doc, ignore_existing_ordered_qty=None): doc['mr_items'] = [] po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') + if not po_items: + frappe.throw(_("Items are required to pull the raw materials which is associated with it.")) + company = doc.get('company') warehouse = doc.get('for_warehouse') From da638cd4763ecab50ea2a5a1fa580cbb5db048ae Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 9 Dec 2019 11:28:31 +0530 Subject: [PATCH 48/76] fix: NoneType' object has no attribute '__getitem_'_ (#19859) --- erpnext/stock/doctype/packed_item/packed_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 831381c86ad..5341f298531 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -65,7 +65,7 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip bin = get_bin_qty(packing_item_code, pi.warehouse) pi.actual_qty = flt(bin.get("actual_qty")) pi.projected_qty = flt(bin.get("projected_qty")) - if old_packed_items_map: + if old_packed_items_map and old_packed_items_map.get((packing_item_code, main_item_row.item_code)): pi.batch_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].batch_no pi.serial_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].serial_no pi.warehouse = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].warehouse From b63101250c9e9bc414bf82cba562b6c8a0c25b49 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:03:24 +0530 Subject: [PATCH 49/76] fix: Rounding Adjustment GL entry fix (#19838) * fix: Rounding Adjustment GL entry fix * fix: Spacing in tab * fix: Comment fix --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 +++++- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3bb3df8dbd9..7b2061ac168 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -830,7 +830,11 @@ class PurchaseInvoice(BuyingController): ) def make_gle_for_rounding_adjustment(self, gl_entries): - if self.rounding_adjustment: + # if rounding adjustment in small and conversion rate is also small then + # base_rounding_adjustment may become zero due to small precision + # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 + # then base_rounding_adjustment becomes zero and error is thrown in GL Entry + if self.rounding_adjustment and self.base_rounding_adjustment: round_off_account, round_off_cost_center = \ get_round_off_account_and_cost_center(self.company) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index d024a31162f..c4d64a72fd1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -953,7 +953,7 @@ class SalesInvoice(SellingController): ) def make_gle_for_rounding_adjustment(self, gl_entries): - if flt(self.rounding_adjustment, self.precision("rounding_adjustment")): + if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment: round_off_account, round_off_cost_center = \ get_round_off_account_and_cost_center(self.company) From babc2d1ea4ab382667602916b8ad91f3d16d9ca7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:04:44 +0530 Subject: [PATCH 50/76] fix: Consistency in button positions in Sales Order and Purchase Order (#19833) --- erpnext/selling/doctype/sales_order/sales_order.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 7dc58b582ac..2dae0d8063d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -112,7 +112,6 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( let allow_delivery = false; if (doc.docstatus==1) { - this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); if(this.frm.has_perm("submit")) { if(doc.status === 'On Hold') { @@ -136,7 +135,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( if(doc.status !== 'Closed') { if(doc.status !== 'On Hold') { - allow_delivery = this.frm.doc.items.some(item => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty)) + allow_delivery = this.frm.doc.items.some(item => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty)) && !this.frm.doc.skip_delivery_note if (this.frm.has_perm("submit")) { @@ -148,6 +147,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } + this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + // delivery note if(flt(doc.per_delivered, 6) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) { this.frm.add_custom_button(__('Delivery Note'), () => this.make_delivery_note_based_on_delivery_date(), __('Create')); @@ -361,7 +362,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }, toggle_delivery_date: function() { - this.frm.fields_dict.items.grid.toggle_reqd("delivery_date", + this.frm.fields_dict.items.grid.toggle_reqd("delivery_date", (this.frm.doc.order_type == "Sales" && !this.frm.doc.skip_delivery_note)); }, From 39e4f75a76471ce8846d886700b175999cc112dd Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 9 Dec 2019 13:16:35 +0530 Subject: [PATCH 51/76] fix: get outstanding invoices btn working on submitted payment entry (#19858) --- erpnext/accounts/doctype/payment_entry/payment_entry.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index acfc660c4f7..997937738b6 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -332,6 +332,7 @@ "label": "Reference" }, { + "depends_on": "eval:doc.docstatus==0", "fieldname": "get_outstanding_invoice", "fieldtype": "Button", "label": "Get Outstanding Invoice" @@ -575,7 +576,7 @@ } ], "is_submittable": 1, - "modified": "2019-11-06 12:59:43.151721", + "modified": "2019-12-08 13:02:30.016610", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From 4350846f1e3e4431ca1a23165b2699d3c3d3327c Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 9 Dec 2019 14:27:38 +0530 Subject: [PATCH 52/76] fix(compensatory leave request): Create leave ledger entry on submit/cancel (#19476) * fix: allow creation of leave ledger entry * feat(compensatory-leave-request): create leave ledger entry on submit * style: add descriptive comments * test: allow creation of leave period based on company * fix(leave-allocation): check name of leave allocation for determining overlap * fix: validate new leaves allocated before updating leave allocation * test: compensatory leave request creation * fix: skip leave entries for non expired leave allocation * test: creation of leave ledger entry on creation of compensatory leave request * fix: minor changes * fix: fetch leave approver defined in employee in leave application * fix: attendance creation and leave balance calculation * test: create leave period for the compensatory off * style: add descriptive method name * test: updation of leave allocation on submit * fix: remove db_set from compensatory off --- .../compensatory_leave_request.py | 73 ++++---- .../test_compensatory_leave_request.py | 157 ++++++++++++++---- .../leave_allocation/leave_allocation.py | 12 +- .../leave_application/leave_application.js | 4 +- .../leave_application/leave_application.py | 23 +-- .../test_leave_application.py | 4 +- .../doctype/leave_period/test_leave_period.py | 12 +- erpnext/hr/utils.py | 9 +- 8 files changed, 205 insertions(+), 89 deletions(-) diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index bc4a1b4034b..7a9727f18c4 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, add_days, getdate +from frappe.utils import date_diff, add_days, getdate, cint from frappe.model.document import Document -from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, get_holidays_for_employee +from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ + get_holidays_for_employee, create_additional_leave_ledger_entry class CompensatoryLeaveRequest(Document): @@ -25,16 +26,14 @@ class CompensatoryLeaveRequest(Document): frappe.throw(_("Leave Type is madatory")) def validate_attendance(self): - query = """select attendance_date, status - from `tabAttendance` where - attendance_date between %(work_from_date)s and %(work_end_date)s - and docstatus=1 and status = 'Present' and employee=%(employee)s""" + attendance = frappe.get_all('Attendance', + filters={ + 'attendance_date': ['between', (self.work_from_date, self.work_end_date)], + 'status': 'Present', + 'docstatus': 1, + 'employee': self.employee + }, fields=['attendance_date', 'status']) - attendance = frappe.db.sql(query, { - "work_from_date": self.work_from_date, - "work_end_date": self.work_end_date, - "employee": self.employee - }, as_dict=True) if len(attendance) < date_diff(self.work_end_date, self.work_from_date) + 1: frappe.throw(_("You are not present all day(s) between compensatory leave request days")) @@ -50,13 +49,19 @@ class CompensatoryLeaveRequest(Document): date_difference -= 0.5 leave_period = get_leave_period(self.work_from_date, self.work_end_date, company) if leave_period: - leave_allocation = self.exists_allocation_for_period(leave_period) + leave_allocation = self.get_existing_allocation_for_period(leave_period) if leave_allocation: leave_allocation.new_leaves_allocated += date_difference - leave_allocation.submit() + leave_allocation.validate() + leave_allocation.db_set("new_leaves_allocated", leave_allocation.total_leaves_allocated) + leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) + + # generate additional ledger entry for the new compensatory leaves off + create_additional_leave_ledger_entry(leave_allocation, date_difference, add_days(self.work_end_date, 1)) + else: leave_allocation = self.create_leave_allocation(leave_period, date_difference) - self.db_set("leave_allocation", leave_allocation.name) + self.leave_allocation=leave_allocation.name else: frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date)) @@ -68,11 +73,16 @@ class CompensatoryLeaveRequest(Document): leave_allocation = frappe.get_doc("Leave Allocation", self.leave_allocation) if leave_allocation: leave_allocation.new_leaves_allocated -= date_difference - if leave_allocation.total_leaves_allocated - date_difference <= 0: - leave_allocation.total_leaves_allocated = 0 - leave_allocation.submit() + if leave_allocation.new_leaves_allocated - date_difference <= 0: + leave_allocation.new_leaves_allocated = 0 + leave_allocation.validate() + leave_allocation.db_set("new_leaves_allocated", leave_allocation.total_leaves_allocated) + leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) - def exists_allocation_for_period(self, leave_period): + # create reverse entry on cancelation + create_additional_leave_ledger_entry(leave_allocation, date_difference * -1, add_days(self.work_end_date, 1)) + + def get_existing_allocation_for_period(self, leave_period): leave_allocation = frappe.db.sql(""" select name from `tabLeave Allocation` @@ -95,17 +105,18 @@ class CompensatoryLeaveRequest(Document): def create_leave_allocation(self, leave_period, date_difference): is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward") - allocation = frappe.new_doc("Leave Allocation") - allocation.employee = self.employee - allocation.employee_name = self.employee_name - allocation.leave_type = self.leave_type - allocation.from_date = add_days(self.work_end_date, 1) - allocation.to_date = leave_period[0].to_date - allocation.new_leaves_allocated = date_difference - allocation.total_leaves_allocated = date_difference - allocation.description = self.reason - if is_carry_forward == 1: - allocation.carry_forward = True - allocation.save(ignore_permissions = True) + allocation = frappe.get_doc(dict( + doctype="Leave Allocation", + employee=self.employee, + employee_name=self.employee_name, + leave_type=self.leave_type, + from_date=add_days(self.work_end_date, 1), + to_date=leave_period[0].to_date, + carry_forward=cint(is_carry_forward), + new_leaves_allocated=date_difference, + total_leaves_allocated=date_difference, + description=self.reason + )) + allocation.insert(ignore_permissions=True) allocation.submit() - return allocation + return allocation \ No newline at end of file diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py index f2ca1f4f5f0..1615ab30f1d 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py @@ -5,37 +5,128 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.utils import today, add_months, add_days +from erpnext.hr.doctype.attendance_request.test_attendance_request import get_employee +from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period +from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on -# class TestCompensatoryLeaveRequest(unittest.TestCase): -# def get_compensatory_leave_request(self): -# return frappe.get_doc('Compensatory Leave Request', dict( -# employee = employee, -# work_from_date = today, -# work_to_date = today, -# reason = 'test' -# )).insert() -# -# def test_creation_of_leave_allocation(self): -# employee = get_employee() -# today = get_today() -# -# compensatory_leave_request = self.get_compensatory_leave_request(today) -# -# before = get_leave_balance(employee, compensatory_leave_request.leave_type) -# -# compensatory_leave_request.submit() -# -# self.assertEqual(get_leave_balance(employee, compensatory_leave_request.leave_type), before + 1) -# -# def test_max_compensatory_leave(self): -# employee = get_employee() -# today = get_today() -# -# compensatory_leave_request = self.get_compensatory_leave_request() -# -# frappe.db.set_value('Leave Type', compensatory_leave_request.leave_type, 'max_leaves_allowed', 0) -# -# self.assertRaises(MaxLeavesLimitCrossed, compensatory_leave_request.submit) -# -# frappe.db.set_value('Leave Type', compensatory_leave_request.leave_type, 'max_leaves_allowed', 10) -# +class TestCompensatoryLeaveRequest(unittest.TestCase): + def setUp(self): + frappe.db.sql(''' delete from `tabCompensatory Leave Request`''') + frappe.db.sql(''' delete from `tabLeave Ledger Entry`''') + frappe.db.sql(''' delete from `tabLeave Allocation`''') + frappe.db.sql(''' delete from `tabAttendance` where attendance_date in {0} '''.format((today(), add_days(today(), -1)))) #nosec + create_leave_period(add_months(today(), -3), add_months(today(), 3), "_Test Company") + create_holiday_list() + + employee = get_employee() + employee.holiday_list = "_Test Compensatory Leave" + employee.save() + + def test_leave_balance_on_submit(self): + ''' check creation of leave allocation on submission of compensatory leave request ''' + employee = get_employee() + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + + before = get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, today()) + compensatory_leave_request.submit() + + self.assertEqual(get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, add_days(today(), 1)), before + 1) + + def test_leave_allocation_update_on_submit(self): + employee = get_employee() + mark_attendance(employee, date=add_days(today(), -1)) + compensatory_leave_request = get_compensatory_leave_request(employee.name, leave_date=add_days(today(), -1)) + compensatory_leave_request.submit() + + # leave allocation creation on submit + leaves_allocated = frappe.db.get_value('Leave Allocation', { + 'name': compensatory_leave_request.leave_allocation + }, ['total_leaves_allocated']) + self.assertEqual(leaves_allocated, 1) + + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + compensatory_leave_request.submit() + + # leave allocation updates on submission of second compensatory leave request + leaves_allocated = frappe.db.get_value('Leave Allocation', { + 'name': compensatory_leave_request.leave_allocation + }, ['total_leaves_allocated']) + self.assertEqual(leaves_allocated, 2) + + def test_creation_of_leave_ledger_entry_on_submit(self): + ''' check creation of leave ledger entry on submission of leave request ''' + employee = get_employee() + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + compensatory_leave_request.submit() + + filters = dict(transaction_name=compensatory_leave_request.leave_allocation) + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters) + + self.assertEquals(len(leave_ledger_entry), 1) + self.assertEquals(leave_ledger_entry[0].employee, compensatory_leave_request.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, 1) + + # check reverse leave ledger entry on cancellation + compensatory_leave_request.cancel() + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters, order_by = 'creation desc') + + self.assertEquals(len(leave_ledger_entry), 2) + self.assertEquals(leave_ledger_entry[0].employee, compensatory_leave_request.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, -1) + +def get_compensatory_leave_request(employee, leave_date=today()): + prev_comp_leave_req = frappe.db.get_value('Compensatory Leave Request', + dict(leave_type='Compensatory Off', + work_from_date=leave_date, + work_end_date=leave_date, + employee=employee), 'name') + if prev_comp_leave_req: + return frappe.get_doc('Compensatory Leave Request', prev_comp_leave_req) + + return frappe.get_doc(dict( + doctype='Compensatory Leave Request', + employee=employee, + leave_type='Compensatory Off', + work_from_date=leave_date, + work_end_date=leave_date, + reason='test' + )).insert() + +def mark_attendance(employee, date=today(), status='Present'): + if not frappe.db.exists(dict(doctype='Attendance', employee=employee.name, attendance_date=date, status='Present')): + attendance = frappe.get_doc({ + "doctype": "Attendance", + "employee": employee.name, + "attendance_date": date, + "status": status + }) + attendance.save() + attendance.submit() + +def create_holiday_list(): + if frappe.db.exists("Holiday List", "_Test Compensatory Leave"): + return + + holiday_list = frappe.get_doc({ + "doctype": "Holiday List", + "from_date": add_months(today(), -3), + "to_date": add_months(today(), 3), + "holidays": [ + { + "description": "Test Holiday", + "holiday_date": today() + }, + { + "description": "Test Holiday 1", + "holiday_date": add_days(today(), -1) + } + ], + "holiday_list_name": "_Test Compensatory Leave" + }) + holiday_list.save() \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 874ae7a1bc2..d13bb4577cd 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -69,10 +69,14 @@ class LeaveAllocation(Document): def validate_allocation_overlap(self): leave_allocation = frappe.db.sql(""" - select name from `tabLeave Allocation` - where employee=%s and leave_type=%s and docstatus=1 - and to_date >= %s and from_date <= %s""", - (self.employee, self.leave_type, self.from_date, self.to_date)) + SELECT + name + FROM `tabLeave Allocation` + WHERE + employee=%s AND leave_type=%s + AND name <> %s AND docstatus=1 + AND to_date >= %s AND from_date <= %s""", + (self.employee, self.leave_type, self.name, self.from_date, self.to_date)) if leave_allocation: frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}") diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index db3819eff2d..e32d57011d8 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -170,7 +170,7 @@ frappe.ui.form.on("Leave Application", { frm.set_value('to_date', ''); return; } - // server call is done to include holidays in leave days calculations + // server call is done to include holidays in leave days calculations return frappe.call({ method: 'erpnext.hr.doctype.leave_application.leave_application.get_number_of_leave_days', args: { @@ -193,7 +193,7 @@ frappe.ui.form.on("Leave Application", { set_leave_approver: function(frm) { if(frm.doc.employee) { - // server call is done to include holidays in leave days calculations + // server call is done to include holidays in leave days calculations return frappe.call({ method: 'erpnext.hr.doctype.leave_application.leave_application.get_leave_approver', args: { diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 0e6630541c4..65fcbf7a999 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -549,10 +549,10 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date): leave_days += leave_entry.leaves elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \ - and not skip_expiry_leaves(leave_entry, to_date): + and leave_entry.is_expired and not skip_expiry_leaves(leave_entry, to_date): leave_days += leave_entry.leaves - else: + elif leave_entry.transaction_type == 'Leave Application': if leave_entry.from_date < getdate(from_date): leave_entry.from_date = from_date if leave_entry.to_date > getdate(to_date): @@ -579,14 +579,15 @@ def skip_expiry_leaves(leave_entry, date): def get_leave_entries(employee, leave_type, from_date, to_date): ''' Returns leave entries between from_date and to_date ''' return frappe.db.sql(""" - select employee, leave_type, from_date, to_date, leaves, transaction_type, is_carry_forward, transaction_name - from `tabLeave Ledger Entry` - where employee=%(employee)s and leave_type=%(leave_type)s - and docstatus=1 - and leaves<0 - and (from_date between %(from_date)s and %(to_date)s - or to_date between %(from_date)s and %(to_date)s - or (from_date < %(from_date)s and to_date > %(to_date)s)) + SELECT + employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, + is_carry_forward, is_expired + FROM `tabLeave Ledger Entry` + WHERE employee=%(employee)s AND leave_type=%(leave_type)s + AND docstatus=1 AND leaves<0 + AND (from_date between %(from_date)s AND %(to_date)s + OR to_date between %(from_date)s AND %(to_date)s + OR (from_date < %(from_date)s AND to_date > %(to_date)s)) """, { "from_date": from_date, "to_date": to_date, @@ -773,4 +774,4 @@ def get_leave_approver(employee): leave_approver = frappe.db.get_value('Department Approver', {'parent': department, 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') - return leave_approver + return leave_approver \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 38ae808f27e..b9c02101f1b 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -301,7 +301,7 @@ class TestLeaveApplication(unittest.TestCase): to_date = add_days(date, 2), company = "_Test Company", docstatus = 1, - status = "Approved" + status = "Approved" )) leave_application.submit() @@ -314,7 +314,7 @@ class TestLeaveApplication(unittest.TestCase): to_date = add_days(date, 8), company = "_Test Company", docstatus = 1, - status = "Approved" + status = "Approved" )) self.assertRaises(frappe.ValidationError, leave_application.insert) diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 850a08dd536..1762cf917a2 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -43,10 +43,18 @@ class TestLeavePeriod(unittest.TestCase): leave_period.grant_leave_allocation(employee=employee_doc_name) self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20) -def create_leave_period(from_date, to_date): +def create_leave_period(from_date, to_date, company=None): + leave_period = frappe.db.get_value('Leave Period', + dict(company=company or erpnext.get_default_company(), + from_date=from_date, + to_date=to_date, + is_active=1), 'name') + if leave_period: + return frappe.get_doc("Leave Period", leave_period) + leave_period = frappe.get_doc({ "doctype": "Leave Period", - "company": erpnext.get_default_company(), + "company": company or erpnext.get_default_company(), "from_date": from_date, "to_date": to_date, "is_active": 1 diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 1464a779368..c3e8d27557d 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -321,11 +321,11 @@ def allocate_earned_leaves(): if new_allocation == allocation.total_leaves_allocated: continue allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_earned_leave_ledger_entry(allocation, earned_leaves, today) + create_additional_leave_ledger_entry(allocation, earned_leaves, today) -def create_earned_leave_ledger_entry(allocation, earned_leaves, date): - ''' Create leave ledger entry based on the earned leave frequency ''' - allocation.new_leaves_allocated = earned_leaves +def create_additional_leave_ledger_entry(allocation, leaves, date): + ''' Create leave ledger entry for leave types ''' + allocation.new_leaves_allocated = leaves allocation.from_date = date allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() @@ -389,6 +389,7 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): def get_holidays_for_employee(employee, start_date, end_date): holiday_list = get_holiday_list_for_employee(employee) + holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` where parent=%(holiday_list)s From 7e958da6d6062a6d1a363bde8ec42ae4e0d3ec0a Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 9 Dec 2019 17:23:19 +0530 Subject: [PATCH 53/76] fix: sales order reqd only checks for stock items (#19865) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 ++---- .../selling/doctype/selling_settings/selling_settings.json | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c4d64a72fd1..0f4d4451be9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -535,10 +535,8 @@ class SalesInvoice(SellingController): for i in dic: if frappe.db.get_single_value('Selling Settings', dic[i][0]) == 'Yes': for d in self.get('items'): - is_stock_item = frappe.get_cached_value('Item', d.item_code, 'is_stock_item') - if (d.item_code and is_stock_item == 1\ - and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])): - msgprint(_("{0} is mandatory for Stock Item {1}").format(i,d.item_code), raise_exception=1) + if (d.item_code and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])): + msgprint(_("{0} is mandatory for Item {1}").format(i,d.item_code), raise_exception=1) def validate_proj_cust(self): diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 5033d7aa7f9..c04bfd281e2 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -77,7 +77,6 @@ "fieldtype": "Column Break" }, { - "description": "Only for Stock Items", "fieldname": "so_required", "fieldtype": "Select", "label": "Sales Order Required", @@ -138,7 +137,7 @@ "icon": "fa fa-cog", "idx": 1, "issingle": 1, - "modified": "2019-11-25 18:35:51.472653", + "modified": "2019-12-09 13:38:36.486298", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From 9e0b3b29fa71f940eec73ffd514c9d459c3cbdd3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Dec 2019 18:21:23 +0530 Subject: [PATCH 54/76] feat: better message for serial number creation (#19863) --- erpnext/stock/doctype/serial_no/serial_no.py | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 19eb398130c..23d00da7c1f 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -353,17 +353,19 @@ def get_auto_serial_nos(serial_no_series, qty): def auto_make_serial_nos(args): serial_nos = get_serial_nos(args.get('serial_no')) created_numbers = [] + voucher_type = args.get('voucher_type') + item_code = args.get('item_code') for serial_no in serial_nos: if frappe.db.exists("Serial No", serial_no): sr = frappe.get_doc("Serial No", serial_no) sr.via_stock_ledger = True - sr.item_code = args.get('item_code') + sr.item_code = item_code sr.warehouse = args.get('warehouse') if args.get('actual_qty', 0) > 0 else None sr.batch_no = args.get('batch_no') sr.location = args.get('location') sr.company = args.get('company') sr.supplier = args.get('supplier') - if sr.sales_order and args.get('voucher_type') == "Stock Entry" \ + if sr.sales_order and voucher_type == "Stock Entry" \ and not args.get('actual_qty', 0) > 0: sr.sales_order = None sr.save(ignore_permissions=True) @@ -371,10 +373,28 @@ def auto_make_serial_nos(args): created_numbers.append(make_serial_no(serial_no, args)) form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers)) + + # Setting up tranlated title field for all cases + singular_title = _("Serial Number Created") + multiple_title = _("Serial Numbers Created") + + if voucher_type: + multiple_title = singular_title = _("{0} Created").format(voucher_type) + if len(form_links) == 1: - frappe.msgprint(_("Serial No {0} created").format(form_links[0])) + frappe.msgprint(_("Serial No {0} Created").format(form_links[0]), singular_title) elif len(form_links) > 0: - frappe.msgprint(_("The following serial numbers were created:
{0}").format(', '.join(form_links))) + message = _("The following serial numbers were created:

{0}").format(get_items_html(form_links, item_code)) + frappe.msgprint(message, multiple_title) + +def get_items_html(serial_nos, item_code): + body = ', '.join(serial_nos) + return '''
+ {0}: {1} Serial Numbers + +
{2}
+ '''.format(item_code, len(serial_nos), body) + def get_item_details(item_code): return frappe.db.sql("""select name, has_batch_no, docstatus, @@ -397,7 +417,7 @@ def make_serial_no(serial_no, args): sr.via_stock_ledger = args.get('via_stock_ledger') or True sr.asset = args.get('asset') sr.location = args.get('location') - + if args.get('purchase_document_type'): sr.purchase_document_type = args.get('purchase_document_type') From da4847e46d55dbc2647c91ca15ee8648726599f9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 9 Dec 2019 18:27:50 +0530 Subject: [PATCH 55/76] patch: Set against_blanket_order value in existing SO/PO (#19810) * patch: Set against_blanket_order value in existing SO/PO * Update set_against_blanket_order_in_sales_and_purchase_order.py --- erpnext/patches.txt | 3 ++- ..._against_blanket_order_in_sales_and_purchase_order.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index daedca7a28c..68b6cc0f370 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -647,4 +647,5 @@ erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger erpnext.patches.v12_0.update_price_or_product_discount -erpnext.patches.v12_0.set_production_capacity_in_workstation \ No newline at end of file +erpnext.patches.v12_0.set_production_capacity_in_workstation +erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order \ No newline at end of file diff --git a/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py new file mode 100644 index 00000000000..555d8ae7976 --- /dev/null +++ b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py @@ -0,0 +1,9 @@ +import frappe +def execute(): + for doctype in ['Sales Order Item', 'Purchase Order Item']: + frappe.reload_doctype(doctype) + frappe.db.sql(""" + UPDATE `tab{0}` + SET against_blanket_order = 1 + WHERE ifnull(blanket_order, '') != '' + """.format(doctype)) From ae02b4119dae0482334a512b92301f61f2d85201 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Mon, 9 Dec 2019 13:33:35 +0000 Subject: [PATCH 56/76] add init.py for appointment modules (#19823) --- erpnext/www/book-appointment/__init__.py | 0 erpnext/www/book-appointment/verify/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 erpnext/www/book-appointment/__init__.py create mode 100644 erpnext/www/book-appointment/verify/__init__.py diff --git a/erpnext/www/book-appointment/__init__.py b/erpnext/www/book-appointment/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/www/book-appointment/verify/__init__.py b/erpnext/www/book-appointment/verify/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 5998034b02c0bc474769f2ca719e6645beb34ecb Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 9 Dec 2019 19:07:05 +0530 Subject: [PATCH 57/76] fix: error message displays asset category as None (#19873) * fix: error message displays asset category as None * fix: asset gl_entries doesn't considers asset category's cwip account --- erpnext/assets/doctype/asset/asset.py | 10 ++++++++-- .../stock/doctype/purchase_receipt/purchase_receipt.py | 8 +++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d32f834f0f4..3e7f6833a0c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -610,13 +610,19 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non if asset: account = get_asset_category_account(account_name, asset=asset, asset_category = asset_category, company = company) + + if not asset and not account: + account = get_asset_category_account(account_name, asset_category = asset_category, company = company) if not account: account = frappe.get_cached_value('Company', company, account_name) if not account: - frappe.throw(_("Set {0} in asset category {1} or company {2}") - .format(account_name.replace('_', ' ').title(), asset_category, company)) + if not asset_category: + frappe.throw(_("Set {0} in company {2}").format(account_name.replace('_', ' ').title(), company)) + else: + frappe.throw(_("Set {0} in asset category {1} or company {2}") + .format(account_name.replace('_', ' ').title(), asset_category, company)) return account diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d0fae6a2272..9b73d0f2711 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -95,7 +95,8 @@ class PurchaseReceipt(BuyingController): # check cwip accounts before making auto assets # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account arbnb_account = self.get_company_default("asset_received_but_not_billed") - cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ + company = self.company) break def validate_with_previous_doc(self): @@ -364,8 +365,9 @@ class PurchaseReceipt(BuyingController): def add_asset_gl_entries(self, item, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") - # This returns company's default cwip account - cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + # This returns category's cwip account if not then fallback to company's default cwip account + cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ + company = self.company) asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) From 214acc9a42a202f652d33f4b1c9197ac48eb681d Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 9 Dec 2019 20:26:50 +0530 Subject: [PATCH 58/76] fix: Changed check condition and added test --- .../stock/doctype/stock_entry/stock_entry.js | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 9 ++++++- .../doctype/stock_entry/test_stock_entry.py | 27 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index d9c94fced7d..ca480f969d8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -536,7 +536,7 @@ frappe.ui.form.on('Stock Entry Detail', { if(r.message) { var d = locals[cdt][cdn]; $.each(r.message, function(k, v) { - d[k] = v; + frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered }); refresh_field("items"); erpnext.stock.select_batch_and_serial_no(frm, d); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2b99f72565c..913656ad020 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -27,6 +27,7 @@ class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass +class TotalBasicAmountZeroError(frappe.ValidationError): pass from erpnext.controllers.stock_controller import StockController @@ -649,6 +650,12 @@ class StockEntry(StockController): gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse]) + + if self.get("additional_costs") and not total_basic_amount: + #If additional costs table is populated and total basic amount is + #somehow 0, interrupt transaction. + frappe.throw(_("Total Basic Amount in Items Table cannot be 0"), TotalBasicAmountZeroError) + item_account_wise_additional_cost = {} for t in self.get("additional_costs"): @@ -657,7 +664,7 @@ class StockEntry(StockController): item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ - (t.amount * d.basic_amount) / total_basic_amount if total_basic_amount else 0 + (t.amount * d.basic_amount) / total_basic_amount if item_account_wise_additional_cost: for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index eddab5d79dc..c5e67092d3d 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError +from erpnext.stock.doctype.stock_entry.stock_entry import TotalBasicAmountZeroError from six import iteritems def get_sle(**args): @@ -790,6 +791,32 @@ class TestStockEntry(unittest.TestCase): filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, fieldname="is_opening") self.assertEqual(is_opening, "Yes") + def test_total_basic_amount_zero(self): + se = frappe.get_doc({"doctype":"Stock Entry", + "purpose":"Material Receipt", + "stock_entry_type":"Material Receipt", + "posting_date": nowdate(), + "company":"_Test Company with perpetual inventory", + "items":[ + {"item_code":"Basil Leaves", + "description":"Basil Leaves", + "qty": 1, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1"} + ], + "additional_costs":[ + {"expense_account":"Miscellaneous Expenses - TCP1", + "amount":100, + "description": "miscellanous"} + ] + }) + + se.insert() + self.assertRaises(TotalBasicAmountZeroError, se.submit) + def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) se.get("items")[0].item_code = item_code or "_Test Serialized Item With Series" From 6f68f2d02048fc1dca795dd7e610e7478b52f805 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 10 Dec 2019 08:40:10 +0530 Subject: [PATCH 59/76] feat: Cost center for each expenses in expense claim and allowed deferred expense (#19807) * refactor: Expense Claim Cost Center * refactor: Expense Claim Cost Center * refactor: Expense Claim Cost Center * fix: copy cost center from other row * patch: set cost center in children based on parent * fix: test cases for expense claim --- .../hr/doctype/expense_claim/expense_claim.js | 96 +++--- .../doctype/expense_claim/expense_claim.json | 3 +- .../hr/doctype/expense_claim/expense_claim.py | 7 +- .../expense_claim/test_expense_claim.py | 13 +- .../expense_claim_detail.json | 316 ++---------------- .../expense_claim_type/expense_claim_type.js | 2 +- .../expense_claim_type.json | 225 ++++--------- erpnext/patches.txt | 3 +- ..._center_in_child_table_of_expense_claim.py | 8 + 9 files changed, 160 insertions(+), 513 deletions(-) create mode 100644 erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 0d37c10e9cc..570f2ef4c77 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -42,12 +42,6 @@ cur_frm.cscript.onload = function(doc) { cur_frm.set_value("posting_date", frappe.datetime.get_today()); cur_frm.cscript.clear_sanctioned(doc); } - - cur_frm.fields_dict.employee.get_query = function() { - return { - query: "erpnext.controllers.queries.employee_query" - }; - }; }; cur_frm.cscript.clear_sanctioned = function(doc) { @@ -119,7 +113,7 @@ cur_frm.cscript.calculate_total_amount = function(doc,cdt,cdn){ }; erpnext.expense_claim = { - set_title :function(frm) { + set_title: function(frm) { if (!frm.doc.task) { frm.set_value("title", frm.doc.employee_name); } @@ -131,20 +125,20 @@ erpnext.expense_claim = { frappe.ui.form.on("Expense Claim", { setup: function(frm) { - frm.trigger("set_query_for_cost_center"); - frm.trigger("set_query_for_payable_account"); frm.add_fetch("company", "cost_center", "cost_center"); frm.add_fetch("company", "default_expense_claim_payable_account", "payable_account"); - frm.set_query("employee_advance", "advances", function(doc) { + + frm.set_query("employee_advance", "advances", function() { return { filters: [ ['docstatus', '=', 1], - ['employee', '=', doc.employee], + ['employee', '=', frm.doc.employee], ['paid_amount', '>', 0], ['paid_amount', '>', 'claimed_amount'] ] }; }); + frm.set_query("expense_approver", function() { return { query: "erpnext.hr.doctype.department_approver.department_approver.get_approvers", @@ -154,14 +148,49 @@ frappe.ui.form.on("Expense Claim", { } }; }); - frm.set_query("account_head", "taxes", function(doc) { + + frm.set_query("account_head", "taxes", function() { return { filters: [ - ['company', '=', doc.company], + ['company', '=', frm.doc.company], ['account_type', 'in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation"]] ] }; }); + + frm.set_query("cost_center", "expenses", function() { + return { + filters: { + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("payable_account", function() { + return { + filters: { + "report_type": "Balance Sheet", + "account_type": "Payable", + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("task", function() { + return { + filters: { + 'project': frm.doc.project + } + }; + }); + + frm.set_query("employee", function() { + return { + query: "erpnext.controllers.queries.employee_query" + }; + }); }, onload: function(frm) { @@ -244,30 +273,6 @@ frappe.ui.form.on("Expense Claim", { }); }, - set_query_for_cost_center: function(frm) { - frm.fields_dict["cost_center"].get_query = function() { - return { - filters: { - "company": frm.doc.company, - "is_group": 0 - } - }; - }; - }, - - set_query_for_payable_account: function(frm) { - frm.fields_dict["payable_account"].get_query = function() { - return { - filters: { - "report_type": "Balance Sheet", - "account_type": "Payable", - "company": frm.doc.company, - "is_group": 0 - } - }; - }; - }, - is_paid: function(frm) { frm.trigger("toggle_fields"); }, @@ -329,6 +334,10 @@ frappe.ui.form.on("Expense Claim", { }); frappe.ui.form.on("Expense Claim Detail", { + expenses_add: function(frm, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + frm.script_manager.copy_from_first_row("expenses", row, ["cost_center"]); + }, amount: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; var doc = frm.doc; @@ -341,6 +350,9 @@ frappe.ui.form.on("Expense Claim Detail", { cur_frm.cscript.calculate_total(doc,cdt,cdn); frm.trigger("get_taxes"); frm.trigger("calculate_grand_total"); + }, + cost_center: function(frm, cdt, cdn) { + erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "expenses", "cost_center"); } }); @@ -411,12 +423,4 @@ frappe.ui.form.on("Expense Taxes and Charges", { tax_amount: function(frm, cdt, cdn) { frm.trigger("calculate_total_tax", cdt, cdn); } -}); - -cur_frm.fields_dict['task'].get_query = function(doc) { - return { - filters:{ - 'project': doc.project - } - }; -}; +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 5c2f4901713..b5b6823e1ce 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -43,7 +43,6 @@ "accounting_dimensions_section", "project", "dimension_col_break", - "cost_center", "more_details", "status", "amended_from", @@ -366,7 +365,7 @@ "icon": "fa fa-money", "idx": 1, "is_submittable": 1, - "modified": "2019-11-08 14:13:08.964547", + "modified": "2019-11-09 14:13:08.964547", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 59391505fa0..dfb0bb96d82 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -127,7 +127,7 @@ class ExpenseClaim(AccountsController): "debit": data.sanctioned_amount, "debit_in_account_currency": data.sanctioned_amount, "against": self.employee, - "cost_center": self.cost_center + "cost_center": data.cost_center }) ) @@ -190,8 +190,9 @@ class ExpenseClaim(AccountsController): ) def validate_account_details(self): - if not self.cost_center: - frappe.throw(_("Cost center is required to book an expense claim")) + for data in self.expenses: + if not data.cost_center: + frappe.throw(_("Cost center is required to book an expense claim")) if self.is_paid: if not self.mode_of_payment: diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index b559dfd81d1..6e97f0513d6 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -126,7 +126,7 @@ def generate_taxes(): def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None): employee = frappe.db.get_value("Employee", {"status": "Active"}) - currency = frappe.db.get_value('Company', company, 'default_currency') + currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) expense_claim = { "doctype": "Expense Claim", "employee": employee, @@ -134,12 +134,15 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco "approval_status": "Approved", "company": company, 'currency': currency, - "expenses": - [{"expense_type": "Travel", + "expenses": [{ + "expense_type": "Travel", "default_account": account, - 'currency': currency, + "currency": currency, "amount": amount, - "sanctioned_amount": sanctioned_amount}]} + "sanctioned_amount": sanctioned_amount, + "cost_center": cost_center + }] + } if taxes: expense_claim.update(taxes) diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index b23fb6af0fe..b60db2c3af0 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -1,378 +1,118 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, "creation": "2013-02-22 01:27:46", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "expense_date", + "column_break_2", + "expense_type", + "default_account", + "section_break_4", + "description", + "section_break_6", + "amount", + "column_break_8", + "sanctioned_amount", + "cost_center" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expense_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": "Expense Date", - "length": 0, - "no_copy": 0, "oldfieldname": "expense_date", "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expense_type", "fieldtype": "Link", - "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": "Expense Claim Type", - "length": 0, - "no_copy": 0, "oldfieldname": "expense_type", "oldfieldtype": "Link", "options": "Expense Claim Type", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "expense_type", "fieldname": "default_account", "fieldtype": "Link", "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": "Default Account", - "length": 0, - "no_copy": 0, "options": "Account", - "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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_4", - "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, - "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 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "", "fieldname": "description", "fieldtype": "Text Editor", - "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": "Description", - "length": 0, - "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Small Text", - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "300px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "300px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_6", - "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, - "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 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "amount", "fieldtype": "Currency", - "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": "Amount", - "length": 0, - "no_copy": 0, "oldfieldname": "claim_amount", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "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_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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "sanctioned_amount", "fieldtype": "Currency", - "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": "Sanctioned Amount", - "length": 0, "no_copy": 1, "oldfieldname": "sanctioned_amount", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-06-10 08:41:36.122565", - "modified_by": "Administrator", + "modified": "2019-11-22 11:57:25.110942", + "modified_by": "jangeles@bai.ph", "module": "HR", "name": "Expense Claim Detail", "owner": "harshada@webnotestech.com", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js index fda6809e6b2..c4877976775 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Expense Claim Type", { return{ filters: { "is_group": 0, - "root_type": "Expense", + "root_type": frm.doc.deferred_expense_account ? "Asset" : "Expense", 'company': d.company } } diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json index d0c41221cc8..e45f640c078 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json @@ -1,181 +1,72 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:expense_type", - "beta": 0, - "creation": "2012-03-27 14:35:55", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:expense_type", + "creation": "2012-03-27 14:35:55", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "deferred_expense_account", + "expense_type", + "description", + "accounts" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expense_type", - "fieldtype": "Data", - "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": "Expense Claim Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "expense_type", - "oldfieldtype": "Data", - "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, + "fieldname": "expense_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Expense Claim Type", + "oldfieldname": "expense_type", + "oldfieldtype": "Data", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "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": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Small Text", - "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, + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Small Text", "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "fieldtype": "Table", - "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": "Accounts", - "length": 0, - "no_copy": 0, - "options": "Expense Claim Account", - "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 + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Expense Claim Account" + }, + { + "default": "0", + "fieldname": "deferred_expense_account", + "fieldtype": "Check", + "label": "Deferred Expense Account" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-18 14:13:43.770829", - "modified_by": "Administrator", - "module": "HR", - "name": "Expense Claim Type", - "owner": "harshada@webnotestech.com", + ], + "icon": "fa fa-flag", + "idx": 1, + "modified": "2019-11-22 12:00:18.710408", + "modified_by": "jangeles@bai.ph", + "module": "HR", + "name": "Expense Claim Type", + "owner": "harshada@webnotestech.com", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Employee", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Employee" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 68b6cc0f370..053cae05672 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -648,4 +648,5 @@ erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger erpnext.patches.v12_0.update_price_or_product_discount erpnext.patches.v12_0.set_production_capacity_in_workstation -erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order \ No newline at end of file +erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order +erpnext.patches.v12_0.set_cost_center_in_child_table_of_expense_claim diff --git a/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py new file mode 100644 index 00000000000..8ba0d79a831 --- /dev/null +++ b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py @@ -0,0 +1,8 @@ +import frappe +def execute(): + frappe.reload_doc('hr', 'doctype', 'expense_claim_detail') + frappe.db.sql(""" + UPDATE `tabExpense Claim Detail` child, `tabExpense Claim` par + SET child.cost_center = par.cost_center + WHERE child.parent = par.name + """) \ No newline at end of file From ce42f48bfbe8ec13dc5d0da44b718eb1355091b0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 10 Dec 2019 12:15:34 +0530 Subject: [PATCH 60/76] fix: Append expense account only if expense account exists (#19880) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7b2061ac168..917acba92c9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -248,7 +248,7 @@ class PurchaseInvoice(BuyingController): def set_against_expense_account(self): against_accounts = [] for item in self.get("items"): - if item.expense_account not in against_accounts: + if item.expense_account and (item.expense_account not in against_accounts): against_accounts.append(item.expense_account) self.against_expense_account = ",".join(against_accounts) From 9cc5c18f36ec8736a688315adb520d995ed0d753 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 10 Dec 2019 15:20:47 +0530 Subject: [PATCH 61/76] fix: removed only custom dashboard --- erpnext/hr/doctype/leave_application/leave_application.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index e32d57011d8..14ffa0ed1f0 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -60,6 +60,7 @@ frappe.ui.form.on("Leave Application", { } } }); + $("div").remove(".form-dashboard-section.custom"); frm.dashboard.add_section( frappe.render_template('leave_application_dashboard', { data: leave_details From 6e2c13f4c86c73c09f13c9167717f602b701de71 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 10 Dec 2019 15:55:05 +0530 Subject: [PATCH 62/76] feat(regional): Auto state wise taxation for GST India (#19469) * feat(regional): Auto state wise taxation for GST India * fix: Update gst category on addition of GSTIN * fix: Codacy and travis fixes * fix: Travis * fix(test): Update GST category only if GSTIN field available * fix: Test Cases * fix: Do not skip accounts if place of supply is not present * fix: Auto GST taxation for SEZ Party types * fix: Automatic taxation for multi state * fix: Codacy and travis fixes * fix: Auto GST template selection in Sales Order * fix: Move inter state check and source state to tax category * fix: Remove unique check from tax template * fix: Remove unique check from tax template * fix: Address fetching logic in Sales * fix: fecth tax template on company address change * fix: fetch company gstin on address change * fix: company_gstin set value fix * fix: Mutiple fixes and code refactor * fix: Add missing semicolon * fix: Company address fetching in sales invoice * fix: Remove print statement * fix: Import functools * fix: Naming fixes and code cleanup * fix: Iteritems compatibility for python 3 --- .../purchase_invoice/regional/india.js | 3 + .../purchase_taxes_and_charges_template.json | 374 +++++------------ .../doctype/sales_invoice/regional/india.js | 5 + .../doctype/sales_invoice/sales_invoice.js | 4 +- .../sales_taxes_and_charges_template.json | 382 +++++------------- erpnext/accounts/party.py | 107 ++--- .../doctype/purchase_order/regional/india.js | 3 + erpnext/hooks.py | 6 +- erpnext/patches.txt | 1 + .../add_export_type_field_in_party_master.py | 40 ++ erpnext/public/js/queries.js | 2 +- erpnext/public/js/utils/party.js | 44 ++ .../gstr_3b_report/test_gstr_3b_report.py | 17 +- erpnext/regional/india/__init__.py | 5 +- erpnext/regional/india/setup.py | 59 ++- erpnext/regional/india/taxes.js | 41 ++ erpnext/regional/india/utils.py | 116 +++++- .../doctype/sales_order/regional/india.js | 3 + .../doctype/sales_order/sales_order.py | 16 +- erpnext/setup/doctype/company/company.py | 25 ++ .../doctype/delivery_note/delivery_note.py | 7 +- .../doctype/delivery_note/regional/india.js | 4 + .../purchase_receipt/regional/india.js | 3 + 23 files changed, 598 insertions(+), 669 deletions(-) create mode 100644 erpnext/accounts/doctype/purchase_invoice/regional/india.js create mode 100644 erpnext/buying/doctype/purchase_order/regional/india.js create mode 100644 erpnext/patches/v12_0/add_export_type_field_in_party_master.py create mode 100644 erpnext/regional/india/taxes.js create mode 100644 erpnext/selling/doctype/sales_order/regional/india.js create mode 100644 erpnext/stock/doctype/delivery_note/regional/india.js create mode 100644 erpnext/stock/doctype/purchase_receipt/regional/india.js diff --git a/erpnext/accounts/doctype/purchase_invoice/regional/india.js b/erpnext/accounts/doctype/purchase_invoice/regional/india.js new file mode 100644 index 00000000000..81488a2c52a --- /dev/null +++ b/erpnext/accounts/doctype/purchase_invoice/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Invoice'); diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json index bc42630d474..a18fec61cf3 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json @@ -1,300 +1,108 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "beta": 0, - "creation": "2013-01-10 16:34:08", - "custom": 0, - "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "allow_rename": 1, + "creation": "2013-01-10 16:34:08", + "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", + "doctype": "DocType", + "document_type": "Setup", + "field_order": [ + "title", + "is_default", + "disabled", + "column_break4", + "company", + "tax_category", + "section_break6", + "taxes" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 1, - "oldfieldname": "title", - "oldfieldtype": "Data", - "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, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "no_copy": 1, + "oldfieldname": "title", + "oldfieldtype": "Data", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_default", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default", - "length": 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, - "unique": 0 - }, + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disabled", - "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, - "unique": 0 - }, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disabled" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "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, - "unique": 0 - }, + "fieldname": "column_break4", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "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, - "unique": 0 - }, + "fieldname": "section_break6", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "taxes", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Purchase Taxes and Charges", - "length": 0, - "no_copy": 0, - "oldfieldname": "purchase_tax_details", - "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges", - "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, - "unique": 0 + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Purchase Taxes and Charges", + "oldfieldname": "purchase_tax_details", + "oldfieldtype": "Table", + "options": "Purchase Taxes and Charges" + }, + { + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-money", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:18:44.095798", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Purchase Taxes and Charges Template", - "owner": "wasim@webnotestech.com", + ], + "icon": "fa fa-money", + "idx": 1, + "modified": "2019-11-25 13:05:26.220275", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Taxes and Charges Template", + "owner": "wasim@webnotestech.com", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Master Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Purchase User" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index c8305e325f6..48fa364faf0 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -1,3 +1,7 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Sales Invoice'); + frappe.ui.form.on("Sales Invoice", { setup: function(frm) { frm.set_query('transporter', function() { @@ -35,4 +39,5 @@ frappe.ui.form.on("Sales Invoice", { }, __("Make")); } } + }); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 2ea74f6d854..7f4ae3c1fc4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -697,8 +697,8 @@ frappe.ui.form.on('Sales Invoice', { if (frm.doc.company) { frappe.call({ - method:"frappe.contacts.doctype.address.address.get_default_address", - args:{ doctype:'Company',name:frm.doc.company}, + method:"erpnext.setup.doctype.company.company.get_default_company_address", + args:{name:frm.doc.company, existing_address: frm.doc.company_address}, callback: function(r){ if (r.message){ frm.set_value("company_address",r.message) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json index 29e15d165fa..19781bdffaa 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json @@ -1,299 +1,119 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "beta": 0, - "creation": "2013-01-10 16:34:09", - "custom": 0, - "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "allow_rename": 1, + "creation": "2013-01-10 16:34:09", + "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "title", + "is_default", + "disabled", + "column_break_3", + "company", + "tax_category", + "section_break_5", + "taxes" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 1, - "oldfieldname": "title", - "oldfieldtype": "Data", - "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, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "no_copy": 1, + "oldfieldname": "title", + "oldfieldtype": "Data", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_default", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default", - "length": 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, - "unique": 0 - }, + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "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, - "unique": 0 - }, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 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, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Company", - "length": 0, - "no_copy": 0, - "oldfieldname": "company", - "oldfieldtype": "Link", - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Link", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 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, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "* Will be calculated in the transaction.", - "fieldname": "taxes", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Taxes and Charges", - "length": 0, - "no_copy": 0, - "oldfieldname": "other_charges", - "oldfieldtype": "Table", - "options": "Sales Taxes and Charges", - "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, - "unique": 0 + "description": "* Will be calculated in the transaction.", + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Sales Taxes and Charges", + "oldfieldname": "other_charges", + "oldfieldtype": "Table", + "options": "Sales Taxes and Charges" + }, + { + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-money", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:18:41.743257", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Taxes and Charges Template", - "owner": "Administrator", + ], + "icon": "fa fa-money", + "idx": 1, + "modified": "2019-11-25 13:06:03.279099", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Taxes and Charges Template", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 1, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "ASC", - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 59936d5116d..156f2181b87 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -23,7 +23,7 @@ class DuplicatePartyAccountError(frappe.ValidationError): pass @frappe.whitelist() def get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, fetch_payment_terms_template=True, - party_address=None, shipping_address=None, pos_profile=None): + party_address=None, company_address=None, shipping_address=None, pos_profile=None): if not party: return {} @@ -31,14 +31,14 @@ def get_party_details(party=None, account=None, party_type="Customer", company=N frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) return _get_party_details(party, account, party_type, company, posting_date, bill_date, price_list, currency, doctype, ignore_permissions, - fetch_payment_terms_template, party_address, shipping_address, pos_profile) + fetch_payment_terms_template, party_address, company_address, shipping_address, pos_profile) def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, - fetch_payment_terms_template=True, party_address=None, shipping_address=None, pos_profile=None): + fetch_payment_terms_template=True, party_address=None, company_address=None,shipping_address=None, pos_profile=None): - out = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) - party = out[party_type.lower()] + party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) + party = party_details[party_type.lower()] if not ignore_permissions and not frappe.has_permission(party_type, "read", party): frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) @@ -46,76 +46,81 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= party = frappe.get_doc(party_type, party) currency = party.default_currency if party.get("default_currency") else get_company_currency(company) - party_address, shipping_address = set_address_details(out, party, party_type, doctype, company, party_address, shipping_address) - set_contact_details(out, party, party_type) - set_other_values(out, party, party_type) - set_price_list(out, party, party_type, price_list, pos_profile) + party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address) + set_contact_details(party_details, party, party_type) + set_other_values(party_details, party, party_type) + set_price_list(party_details, party, party_type, price_list, pos_profile) - out["tax_category"] = get_address_tax_category(party.get("tax_category"), + party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), party_address, shipping_address if party_type != "Supplier" else party_address) - out["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, - customer_group=out.customer_group, supplier_group=out.supplier_group, tax_category=out.tax_category, - billing_address=party_address, shipping_address=shipping_address) + + if not party_details.get("taxes_and_charges"): + party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, + customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, + billing_address=party_address, shipping_address=shipping_address) if fetch_payment_terms_template: - out["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) + party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) - if not out.get("currency"): - out["currency"] = currency + if not party_details.get("currency"): + party_details["currency"] = currency # sales team if party_type=="Customer": - out["sales_team"] = [{ + party_details["sales_team"] = [{ "sales_person": d.sales_person, "allocated_percentage": d.allocated_percentage or None } for d in party.get("sales_team")] # supplier tax withholding category if party_type == "Supplier" and party: - out["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") + party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") - return out + return party_details -def set_address_details(out, party, party_type, doctype=None, company=None, party_address=None, shipping_address=None): +def set_address_details(party_details, party, party_type, doctype=None, company=None, party_address=None, company_address=None, shipping_address=None): billing_address_field = "customer_address" if party_type == "Lead" \ else party_type.lower() + "_address" - out[billing_address_field] = party_address or get_default_address(party_type, party.name) + party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) if doctype: - out.update(get_fetch_values(doctype, billing_address_field, out[billing_address_field])) + party_details.update(get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])) # address display - out.address_display = get_address_display(out[billing_address_field]) + party_details.address_display = get_address_display(party_details[billing_address_field]) # shipping address if party_type in ["Customer", "Lead"]: - out.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) - out.shipping_address = get_address_display(out["shipping_address_name"]) + party_details.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) + party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) if doctype: - out.update(get_fetch_values(doctype, 'shipping_address_name', out.shipping_address_name)) + party_details.update(get_fetch_values(doctype, 'shipping_address_name', party_details.shipping_address_name)) - if doctype and doctype in ['Delivery Note', 'Sales Invoice']: - out.update(get_company_address(company)) - if out.company_address: - out.update(get_fetch_values(doctype, 'company_address', out.company_address)) - get_regional_address_details(out, doctype, company) + if company_address: + party_details.update({'company_address': company_address}) + else: + party_details.update(get_company_address(company)) - elif doctype and doctype == "Purchase Invoice": - out.update(get_company_address(company)) - if out.company_address: - out["shipping_address"] = shipping_address or out["company_address"] - out.shipping_address_display = get_address_display(out["shipping_address"]) - out.update(get_fetch_values(doctype, 'shipping_address', out.shipping_address)) - get_regional_address_details(out, doctype, company) + if doctype and doctype in ['Delivery Note', 'Sales Invoice', 'Sales Order']: + if party_details.company_address: + party_details.update(get_fetch_values(doctype, 'company_address', party_details.company_address)) + get_regional_address_details(party_details, doctype, company) - return out.get(billing_address_field), out.shipping_address_name + elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: + if party_details.company_address: + party_details["shipping_address"] = shipping_address or party_details["company_address"] + party_details.shipping_address_display = get_address_display(party_details["shipping_address"]) + party_details.update(get_fetch_values(doctype, 'shipping_address', party_details.shipping_address)) + get_regional_address_details(party_details, doctype, company) + + return party_details.get(billing_address_field), party_details.shipping_address_name @erpnext.allow_regional -def get_regional_address_details(out, doctype, company): +def get_regional_address_details(party_details, doctype, company): pass -def set_contact_details(out, party, party_type): - out.contact_person = get_default_contact(party_type, party.name) +def set_contact_details(party_details, party, party_type): + party_details.contact_person = get_default_contact(party_type, party.name) - if not out.contact_person: - out.update({ + if not party_details.contact_person: + party_details.update({ "contact_person": None, "contact_display": None, "contact_email": None, @@ -125,22 +130,22 @@ def set_contact_details(out, party, party_type): "contact_department": None }) else: - out.update(get_contact_details(out.contact_person)) + party_details.update(get_contact_details(party_details.contact_person)) -def set_other_values(out, party, party_type): +def set_other_values(party_details, party, party_type): # copy if party_type=="Customer": to_copy = ["customer_name", "customer_group", "territory", "language"] else: to_copy = ["supplier_name", "supplier_group", "language"] for f in to_copy: - out[f] = party.get(f) + party_details[f] = party.get(f) # fields prepended with default in Customer doctype for f in ['currency'] \ + (['sales_partner', 'commission_rate'] if party_type=="Customer" else []): if party.get("default_" + f): - out[f] = party.get("default_" + f) + party_details[f] = party.get("default_" + f) def get_default_price_list(party): """Return default price list for party (Document object)""" @@ -155,7 +160,7 @@ def get_default_price_list(party): return None -def set_price_list(out, party, party_type, given_price_list, pos=None): +def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list price_list = get_permitted_documents('Price List') @@ -173,9 +178,9 @@ def set_price_list(out, party, party_type, given_price_list, pos=None): price_list = get_default_price_list(party) or given_price_list if price_list: - out.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) + party_details.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) - out["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list + party_details["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype): diff --git a/erpnext/buying/doctype/purchase_order/regional/india.js b/erpnext/buying/doctype/purchase_order/regional/india.js new file mode 100644 index 00000000000..42d3995907f --- /dev/null +++ b/erpnext/buying/doctype/purchase_order/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Order'); \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ed5bf9b5d82..2a5e6d8f49a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -246,10 +246,10 @@ doc_events = { "on_trash": "erpnext.regional.check_deletion_permission" }, 'Address': { - 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code'] + 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category'] }, - ('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): { - 'validate': 'erpnext.regional.india.utils.set_place_of_supply' + ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { + 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 053cae05672..e26b1c88a87 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -645,6 +645,7 @@ erpnext.patches.v12_0.replace_accounting_with_accounts_in_home_settings erpnext.patches.v12_0.set_payment_entry_status erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template +erpnext.patches.v12_0.add_export_type_field_in_party_master erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger erpnext.patches.v12_0.update_price_or_product_discount erpnext.patches.v12_0.set_production_capacity_in_workstation diff --git a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py new file mode 100644 index 00000000000..c565b7ecd87 --- /dev/null +++ b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py @@ -0,0 +1,40 @@ +from __future__ import unicode_literals +import frappe +from erpnext.regional.india.setup import make_custom_fields + +def execute(): + + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + make_custom_fields() + + frappe.reload_doctype('Tax Category') + frappe.reload_doctype('Sales Taxes and Charges Template') + frappe.reload_doctype('Purchase Taxes and Charges Template') + + # Create tax category with inter state field checked + tax_category = frappe.db.get_value('Tax Category', {'name': 'OUT OF STATE'}, 'name') + + if not tax_category: + inter_state_category = frappe.get_doc({ + 'doctype': 'Tax Category', + 'title': 'OUT OF STATE', + 'is_inter_state': 1 + }).insert() + + tax_category = inter_state_category.name + + for doctype in ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template'): + template = frappe.db.get_value(doctype, {'is_inter_state': 1, 'disabled': 0}, ['name']) + if template: + frappe.db.set_value(doctype, template, 'tax_category', tax_category) + + frappe.db.sql(""" + DELETE FROM `tabCustom Field` + WHERE fieldname = 'is_inter_state' + AND dt IN ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template') + """) + + diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 84d2113c067..560a5617da5 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -65,7 +65,7 @@ $.extend(erpnext.queries, { frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name))])); } - console.log(frappe.dynamic_link) + return { query: 'frappe.contacts.doctype.address.address.address_query', filters: { diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a8d3888ba0f..99c1b8ae8f3 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -7,6 +7,21 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if(!method) { method = "erpnext.accounts.party.get_party_details"; } + + if (args) { + if (in_list(['Sales Invoice', 'Sales Order', 'Delivery Note'], frm.doc.doctype)) { + if (frm.doc.company_address && (!args.company_address)) { + args.company_address = frm.doc.company_address; + } + } + + if (in_list(['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'], frm.doc.doctype)) { + if (frm.doc.shipping_address && (!args.shipping_address)) { + args.shipping_address = frm.doc.shipping_address; + } + } + } + if(!args) { if((frm.doctype != "Purchase Order" && frm.doc.customer) || (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) { @@ -30,6 +45,35 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { }; } + if (in_list(['Sales Invoice', 'Sales Order', 'Delivery Note'], frm.doc.doctype)) { + if (!args) { + args = { + party: frm.doc.customer || frm.doc.party_name, + party_type: 'Customer' + } + } + if (frm.doc.company_address && (!args.company_address)) { + args.company_address = frm.doc.company_address; + } + + if (frm.doc.shipping_address_name &&(!args.shipping_address_name)) { + args.shipping_address_name = frm.doc.shipping_address_name; + } + } + + if (in_list(['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'], frm.doc.doctype)) { + if (!args) { + args = { + party: frm.doc.supplier, + party_type: 'Supplier' + } + } + + if (frm.doc.shipping_address && (!args.shipping_address)) { + args.shipping_address = frm.doc.shipping_address; + } + } + if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; } diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index fef73d9a0a8..fa6fb706e9b 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -64,7 +64,8 @@ class TestGSTR3BReport(unittest.TestCase): self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18), self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100), self.assertEqual(output["inward_sup"]["isup_details"][0]["inter"], 250) - self.assertEqual(output["itc_elg"]["itc_avl"][4]["iamt"], 45) + self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50) + self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50) def make_sales_invoice(): si = create_sales_invoice(company="_Test Company GST", @@ -158,10 +159,18 @@ def create_purchase_invoices(): pi.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "CGST - _GST", "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 + "description": "CGST @ 9.0", + "rate": 9 + }) + + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "SGST - _GST", + "cost_center": "Main - _GST", + "description": "SGST @ 9.0", + "rate": 9 }) pi.submit() diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index 46c874b2524..0ed98b74eef 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from six import iteritems states = [ '', @@ -79,4 +80,6 @@ state_numbers = { "Uttar Pradesh": "09", "Uttarakhand": "05", "West Bengal": "19", -} \ No newline at end of file +} + +number_state_mapping = {v: k for k, v in iteritems(state_numbers)} \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 756c17dc3b3..14fdba013c7 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -107,7 +107,12 @@ def make_custom_fields(update=True): dict(fieldname='gst_category', label='GST Category', fieldtype='Select', insert_after='gst_section', print_hide=1, options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - fetch_from='supplier.gst_category', fetch_if_empty=1) + fetch_from='supplier.gst_category', fetch_if_empty=1), + dict(fieldname='export_type', label='Export Type', + fieldtype='Select', insert_after='gst_category', print_hide=1, + depends_on='eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='supplier.export_type', + fetch_if_empty=1), ] sales_invoice_gst_category = [ @@ -116,20 +121,21 @@ def make_custom_fields(update=True): dict(fieldname='gst_category', label='GST Category', fieldtype='Select', insert_after='gst_section', print_hide=1, options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1) + fetch_from='customer.gst_category', fetch_if_empty=1), + dict(fieldname='export_type', label='Export Type', + fieldtype='Select', insert_after='gst_category', print_hide=1, + depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='customer.export_type', + fetch_if_empty=1), ] invoice_gst_fields = [ dict(fieldname='invoice_copy', label='Invoice Copy', - fieldtype='Select', insert_after='gst_category', print_hide=1, allow_on_submit=1, + fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'), dict(fieldname='reverse_charge', label='Reverse Charge', fieldtype='Select', insert_after='invoice_copy', print_hide=1, options='Y\nN', default='N'), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='reverse_charge', print_hide=1, - depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax'), dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', fieldtype='Data', insert_after='export_type', print_hide=1), dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='ecommerce_gstin'), @@ -142,13 +148,13 @@ def make_custom_fields(update=True): purchase_invoice_gst_fields = [ dict(fieldname='supplier_gstin', label='Supplier GSTIN', fieldtype='Data', insert_after='supplier_address', - fetch_from='supplier_address.gstin', print_hide=1), + fetch_from='supplier_address.gstin', print_hide=1, read_only=1), dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='shipping_address_display', - fetch_from='shipping_address.gstin', print_hide=1), + fetch_from='shipping_address.gstin', print_hide=1, read_only=1), dict(fieldname='place_of_supply', label='Place of Supply', fieldtype='Data', insert_after='shipping_address', - print_hide=1, read_only=0), + print_hide=1, read_only=1), ] purchase_invoice_itc_fields = [ @@ -167,17 +173,17 @@ def make_custom_fields(update=True): sales_invoice_gst_fields = [ dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', + fieldtype='Data', insert_after='customer_address', read_only=1, fetch_from='customer_address.gstin', print_hide=1), dict(fieldname='customer_gstin', label='Customer GSTIN', fieldtype='Data', insert_after='shipping_address_name', fetch_from='shipping_address_name.gstin', print_hide=1), dict(fieldname='place_of_supply', label='Place of Supply', fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=0), + print_hide=1, read_only=1), dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1), + fetch_from='company_address.gstin', print_hide=1, read_only=1), ] sales_invoice_shipping_fields = [ @@ -194,7 +200,11 @@ def make_custom_fields(update=True): inter_state_gst_field = [ dict(fieldname='is_inter_state', label='Is Inter State', - fieldtype='Check', insert_after='disabled', print_hide=1) + fieldtype='Check', insert_after='disabled', print_hide=1), + dict(fieldname='tax_category_column_break', fieldtype='Column Break', + insert_after='is_inter_state'), + dict(fieldname='gst_state', label='Source State', fieldtype='Select', + options='\n'.join(states), insert_after='company') ] ewaybill_fields = [ @@ -374,8 +384,7 @@ def make_custom_fields(update=True): 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Sales Order': sales_invoice_gst_fields, - 'Sales Taxes and Charges Template': inter_state_gst_field, - 'Purchase Taxes and Charges Template': inter_state_gst_field, + 'Tax Category': inter_state_gst_field, 'Item': [ dict(fieldname='gst_hsn_code', label='HSN/SAC', fieldtype='Link', options='GST HSN Code', insert_after='item_group'), @@ -459,6 +468,15 @@ def make_custom_fields(update=True): 'insert_after': 'gst_transporter_id', 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', 'default': 'Unregistered' + }, + { + 'fieldname': 'export_type', + 'label': 'Export Type', + 'fieldtype': 'Select', + 'insert_after': 'gst_category', + 'default': 'Without Payment of Tax', + 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } ], 'Customer': [ @@ -469,6 +487,15 @@ def make_custom_fields(update=True): 'insert_after': 'customer_type', 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', 'default': 'Unregistered' + }, + { + 'fieldname': 'export_type', + 'label': 'Export Type', + 'fieldtype': 'Select', + 'insert_after': 'gst_category', + 'default': 'Without Payment of Tax', + 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } ] } diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js new file mode 100644 index 00000000000..1e59032db10 --- /dev/null +++ b/erpnext/regional/india/taxes.js @@ -0,0 +1,41 @@ +erpnext.setup_auto_gst_taxation = (doctype) => { + frappe.ui.form.on(doctype, { + company_address: function(frm) { + frm.trigger('get_tax_template'); + }, + shipping_address: function(frm) { + frm.trigger('get_tax_template'); + }, + tax_category: function(frm) { + frm.trigger('get_tax_template'); + }, + get_tax_template: function(frm) { + let party_details = { + 'shipping_address': frm.doc.shipping_address || '', + 'shipping_address_name': frm.doc.shipping_address_name || '', + 'customer_address': frm.doc.customer_address || '', + 'customer': frm.doc.customer, + 'supplier': frm.doc.supplier, + 'supplier_gstin': frm.doc.supplier_gstin, + 'company_gstin': frm.doc.company_gstin, + 'tax_category': frm.doc.tax_category + }; + + frappe.call({ + method: 'erpnext.regional.india.utils.get_regional_address_details', + args: { + party_details: JSON.stringify(party_details), + doctype: frm.doc.doctype, + company: frm.doc.company, + return_taxes: 1 + }, + callback: function(r) { + if(r.message) { + frm.set_value('taxes_and_charges', r.message.taxes_and_charges); + } + } + }); + } + }); +}; + diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index aae07797a15..77bcc80abab 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -7,6 +7,8 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_ from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.hr.utils import get_salary_assignment from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip +from erpnext.regional.india import number_state_mapping +from six import string_types def validate_gstin_for_india(doc, method): if hasattr(doc, 'gst_state') and doc.gst_state: @@ -46,6 +48,14 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") .format(doc.gst_state_number)) +def update_gst_category(doc, method): + for link in doc.links: + if link.link_doctype in ['Customer', 'Supplier']: + if doc.get('gstin'): + frappe.db.sql(""" + UPDATE `tab{0}` SET gst_category = %s WHERE name = %s AND gst_category = 'Unregistered' + """.format(link.link_doctype), ("Registered Regular", link.link_name)) #nosec + def set_gst_state_and_state_number(doc): if not doc.gst_state: if not doc.state: @@ -122,44 +132,106 @@ def test_method(): '''test function''' return 'overridden' -def get_place_of_supply(out, doctype): +def get_place_of_supply(party_details, doctype): if not frappe.get_meta('Address').has_field('gst_state'): return - if doctype in ("Sales Invoice", "Delivery Note"): - address_name = out.shipping_address_name or out.customer_address - elif doctype == "Purchase Invoice": - address_name = out.shipping_address or out.supplier_address + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + address_name = party_details.shipping_address_name or party_details.customer_address + elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): + address_name = party_details.shipping_address or party_details.supplier_address if address_name: address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number"], as_dict=1) if address and address.gst_state and address.gst_state_number: return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) -def get_regional_address_details(out, doctype, company): - out.place_of_supply = get_place_of_supply(out, doctype) +@frappe.whitelist() +def get_regional_address_details(party_details, doctype, company, return_taxes=None): - if not out.place_of_supply: return + if isinstance(party_details, string_types): + party_details = json.loads(party_details) + party_details = frappe._dict(party_details) - if doctype in ("Sales Invoice", "Delivery Note"): + party_details.place_of_supply = get_place_of_supply(party_details, doctype) + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" - if not out.company_gstin: - return - elif doctype == "Purchase Invoice": - master_doctype = "Purchase Taxes and Charges Template" - if not out.supplier_gstin: + + get_tax_template_for_sez(party_details, master_doctype, company, 'Customer') + get_tax_template_based_on_category(master_doctype, company, party_details) + + if party_details.get('taxes_and_charges') and return_taxes: + return party_details + + if not party_details.company_gstin: return - if ((doctype in ("Sales Invoice", "Delivery Note") and out.company_gstin - and out.company_gstin[:2] != out.place_of_supply[:2]) or (doctype == "Purchase Invoice" - and out.supplier_gstin and out.supplier_gstin[:2] != out.place_of_supply[:2])): - default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0}) + elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): + master_doctype = "Purchase Taxes and Charges Template" + + get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier') + get_tax_template_based_on_category(master_doctype, company, party_details) + + if party_details.get('taxes_and_charges') and return_taxes: + return party_details + + if not party_details.supplier_gstin: + return + + if not party_details.place_of_supply: return + + if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin + and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", + "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): + default_tax = get_tax_template(master_doctype, company, 1, party_details.company_gstin[:2]) else: - default_tax = frappe.db.get_value(master_doctype, {"company": company, "disabled":0, "is_default": 1}) + default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) if not default_tax: return - out["taxes_and_charges"] = default_tax - out.taxes = get_taxes_and_charges(master_doctype, default_tax) + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + + if return_taxes: + return party_details + +def get_tax_template_based_on_category(master_doctype, company, party_details): + if not party_details.get('tax_category'): + return + + default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, + 'name') + + if default_tax: + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + +def get_tax_template(master_doctype, company, is_inter_state, state_code): + tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], + filters = {'is_inter_state': is_inter_state}) + + default_tax = '' + + for tax_category in tax_categories: + if tax_category.gst_state == number_state_mapping[state_code] or \ + (not default_tax and not tax_category.gst_state): + default_tax = frappe.db.get_value(master_doctype, + {'disabled': 0, 'tax_category': tax_category.name}, 'name') + + return default_tax + +def get_tax_template_for_sez(party_details, master_doctype, company, party_type): + + gst_details = frappe.db.get_value(party_type, {'name': party_details.get(frappe.scrub(party_type))}, + ['gst_category', 'export_type'], as_dict=1) + + if gst_details: + if gst_details.gst_category == 'SEZ' and gst_details.export_type == 'With Payment of Tax': + default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0, + "gst_state": number_state_mapping[party_details.company_gstin[:2]]}) + + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + def calculate_annual_eligible_hra_exemption(doc): basic_component = frappe.get_cached_value('Company', doc.company, "basic_component") @@ -555,7 +627,7 @@ def get_gst_accounts(company, account_wise=False): filters={"parent": "GST Settings", "company": company}, fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) - if not gst_settings_accounts: + if not gst_settings_accounts and not frappe.flags.in_test: frappe.throw(_("Please set GST Accounts in GST Settings")) for d in gst_settings_accounts: diff --git a/erpnext/selling/doctype/sales_order/regional/india.js b/erpnext/selling/doctype/sales_order/regional/india.js new file mode 100644 index 00000000000..c11cfcc50b7 --- /dev/null +++ b/erpnext/selling/doctype/sales_order/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Sales Order'); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e97a4ee4611..2112a4174b1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -578,8 +578,12 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") - # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Delivery Note", 'company_address', target.company_address)) @@ -645,8 +649,12 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") - # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 04e8a83e7f5..2eee919b530 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -14,6 +14,8 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils.nestedset import NestedSet +import functools + class Company(NestedSet): nsm_parent_field = 'parent_company' @@ -560,3 +562,26 @@ def get_timeline_data(doctype, name): return json.loads(history) if history and '{' in history else {} return date_to_value_dict + +@frappe.whitelist() +def get_default_company_address(name, sort_key='is_primary_address', existing_address=None): + if sort_key not in ['is_shipping_address', 'is_primary_address']: + return None + + out = frappe.db.sql(""" SELECT + addr.name, addr.%s + FROM + `tabAddress` addr, `tabDynamic Link` dl + WHERE + dl.parent = addr.name and dl.link_doctype = 'Company' and + dl.link_name = %s and ifnull(addr.disabled, 0) = 0 + """ %(sort_key, '%s'), (name)) #nosec + + if existing_address: + if existing_address in [d[0] for d in out]: + return existing_address + + if out: + return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0] + else: + return None \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c98dfe35fb5..39aad2e0071 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -424,7 +424,12 @@ def make_sales_invoice(source_name, target_doc=None): target.run_method("calculate_taxes_and_totals") # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) diff --git a/erpnext/stock/doctype/delivery_note/regional/india.js b/erpnext/stock/doctype/delivery_note/regional/india.js new file mode 100644 index 00000000000..22f4716ea52 --- /dev/null +++ b/erpnext/stock/doctype/delivery_note/regional/india.js @@ -0,0 +1,4 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Delivery Note'); + diff --git a/erpnext/stock/doctype/purchase_receipt/regional/india.js b/erpnext/stock/doctype/purchase_receipt/regional/india.js new file mode 100644 index 00000000000..b4f1201f36c --- /dev/null +++ b/erpnext/stock/doctype/purchase_receipt/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Receipt'); \ No newline at end of file From 664d0d89b53f4a39df3566a4c87694e432c234c3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 10 Dec 2019 21:32:17 +0530 Subject: [PATCH 63/76] fix: incorrect account mapping for child companies (#19887) * fix: incorrect account mapping for child companies on adding account to parent company * Update account.py --- erpnext/accounts/doctype/account/account.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index cccced8e0be..cf1748f6a7f 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -109,12 +109,13 @@ class Account(NestedSet): if not descendants: return parent_acc_name_map = {} - parent_acc_name = frappe.db.get_value('Account', self.parent_account, "account_name") + parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \ + ["account_name", "account_number"]) for d in frappe.db.get_values('Account', - {"company": ["in", descendants], "account_name": parent_acc_name}, + { "company": ["in", descendants], "account_name": parent_acc_name, + "account_number": parent_acc_number }, ["company", "name"], as_dict=True): parent_acc_name_map[d["company"]] = d["name"] - if not parent_acc_name_map: return self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) From c58495dceedad0031fa946abc862c0de37956c26 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 10 Dec 2019 21:34:03 +0530 Subject: [PATCH 64/76] fix: Item-wise Sales History report not working (#19889) --- .../item_wise_sales_history/item_wise_sales_history.js | 4 ++++ .../item_wise_sales_history/item_wise_sales_history.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js index ee806a78fb4..daca2e3bd0c 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js @@ -20,11 +20,15 @@ frappe.query_reports["Item-wise Sales History"] = { }, { fieldname:"from_date", + reqd: 1, label: __("From Date"), fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { fieldname:"to_date", + reqd: 1, + default: frappe.datetime.get_today(), label: __("To Date"), fieldtype: "Date", }, diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 226c34f735a..1fc3663bed7 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -196,6 +196,7 @@ def get_customer_details(): def get_sales_order_details(company_list, filters): conditions = get_conditions(filters) + return frappe.db.sql(""" SELECT so_item.item_code, so_item.item_name, so_item.item_group, @@ -208,7 +209,6 @@ def get_sales_order_details(company_list, filters): `tabSales Order` so, `tabSales Order Item` so_item WHERE so.name = so_item.parent - AND so.company in (%s) - AND so.docstatus = 1 - {0} - """.format(conditions), company_list, as_dict=1) #nosec + AND so.company in ({0}) + AND so.docstatus = 1 {1} + """.format(','.join(["%s"] * len(company_list)), conditions), tuple(company_list), as_dict=1) From 6aa763221b658453d59b01706ce788ea0914d7a3 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Tue, 10 Dec 2019 16:05:28 +0000 Subject: [PATCH 65/76] fix: rename appointment booking route (#19886) * rename appoinment booking route * fix: replace all references to book-appointment route --- erpnext/crm/doctype/appointment/appointment.py | 2 +- erpnext/support/web_form/issues/issues.json | 2 +- .../{book-appointment => book_appointment}/__init__.py | 0 .../www/{book-appointment => book_appointment}/index.css | 0 .../www/{book-appointment => book_appointment}/index.html | 2 +- .../www/{book-appointment => book_appointment}/index.js | 8 ++++---- .../www/{book-appointment => book_appointment}/index.py | 0 .../verify/__init__.py | 0 .../verify/index.html | 0 .../verify/index.py | 0 10 files changed, 7 insertions(+), 7 deletions(-) rename erpnext/www/{book-appointment => book_appointment}/__init__.py (100%) rename erpnext/www/{book-appointment => book_appointment}/index.css (100%) rename erpnext/www/{book-appointment => book_appointment}/index.html (97%) rename erpnext/www/{book-appointment => book_appointment}/index.js (96%) rename erpnext/www/{book-appointment => book_appointment}/index.py (100%) rename erpnext/www/{book-appointment => book_appointment}/verify/__init__.py (100%) rename erpnext/www/{book-appointment => book_appointment}/verify/index.html (100%) rename erpnext/www/{book-appointment => book_appointment}/verify/index.py (100%) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 2affba2ac40..b6c4c4707de 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -171,7 +171,7 @@ class Appointment(Document): self.save(ignore_permissions=True) def _get_verify_url(self): - verify_route = '/book-appointment/verify' + verify_route = '/book_appointment/verify' params = { 'email': self.customer_email, 'appointment': self.name diff --git a/erpnext/support/web_form/issues/issues.json b/erpnext/support/web_form/issues/issues.json index 652114f7382..9b904ad7962 100644 --- a/erpnext/support/web_form/issues/issues.json +++ b/erpnext/support/web_form/issues/issues.json @@ -18,7 +18,7 @@ "is_standard": 1, "login_required": 1, "max_attachment_size": 0, - "modified": "2019-06-27 22:58:49.609672", + "modified": "2019-12-10 13:48:19.894186", "modified_by": "Administrator", "module": "Support", "name": "issues", diff --git a/erpnext/www/book-appointment/__init__.py b/erpnext/www/book_appointment/__init__.py similarity index 100% rename from erpnext/www/book-appointment/__init__.py rename to erpnext/www/book_appointment/__init__.py diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book_appointment/index.css similarity index 100% rename from erpnext/www/book-appointment/index.css rename to erpnext/www/book_appointment/index.css diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book_appointment/index.html similarity index 97% rename from erpnext/www/book-appointment/index.html rename to erpnext/www/book_appointment/index.html index 96774d5656b..f242f43ad54 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book_appointment/index.html @@ -4,7 +4,7 @@ {% block script %} - + {% endblock %} {% block page_content %} diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book_appointment/index.js similarity index 96% rename from erpnext/www/book-appointment/index.js rename to erpnext/www/book_appointment/index.js index 13c87ddbcff..c8dd5013d5c 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -15,10 +15,10 @@ async function initialise_select_date() { async function get_global_variables() { // Using await through this file instead of then. window.appointment_settings = (await frappe.call({ - method: 'erpnext.www.book-appointment.index.get_appointment_settings' + method: 'erpnext.www.book_appointment.index.get_appointment_settings' })).message; window.timezones = (await frappe.call({ - method:'erpnext.www.book-appointment.index.get_timezones' + method:'erpnext.www.book_appointment.index.get_timezones' })).message; window.holiday_list = window.appointment_settings.holiday_list; } @@ -79,7 +79,7 @@ function on_date_or_timezone_select() { async function get_time_slots(date, timezone) { let slots = (await frappe.call({ - method: 'erpnext.www.book-appointment.index.get_appointment_slots', + method: 'erpnext.www.book_appointment.index.get_appointment_slots', args: { date: date, timezone: timezone @@ -201,7 +201,7 @@ async function submit() { } let contact = get_form_data(); let appointment = frappe.call({ - method: 'erpnext.www.book-appointment.index.create_appointment', + method: 'erpnext.www.book_appointment.index.create_appointment', args: { 'date': window.selected_date, 'time': window.selected_time, diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book_appointment/index.py similarity index 100% rename from erpnext/www/book-appointment/index.py rename to erpnext/www/book_appointment/index.py diff --git a/erpnext/www/book-appointment/verify/__init__.py b/erpnext/www/book_appointment/verify/__init__.py similarity index 100% rename from erpnext/www/book-appointment/verify/__init__.py rename to erpnext/www/book_appointment/verify/__init__.py diff --git a/erpnext/www/book-appointment/verify/index.html b/erpnext/www/book_appointment/verify/index.html similarity index 100% rename from erpnext/www/book-appointment/verify/index.html rename to erpnext/www/book_appointment/verify/index.html diff --git a/erpnext/www/book-appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py similarity index 100% rename from erpnext/www/book-appointment/verify/index.py rename to erpnext/www/book_appointment/verify/index.py From 9046c13858be493e53be47312fbb3d5c1670cb2d Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Tue, 10 Dec 2019 21:37:27 +0530 Subject: [PATCH 66/76] fix: added extra condition (#19884) --- erpnext/hr/doctype/leave_application/leave_application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 65fcbf7a999..915cea149d8 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -351,7 +351,7 @@ class LeaveApplication(Document): pass def create_leave_ledger_entry(self, submit=True): - if self.status != 'Approved': + if self.status != 'Approved' and submit: return expiry_date = get_allocation_expiry(self.employee, self.leave_type, From 5380a4c3db67cf8e69fce0c7c06661c893a4fee4 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Tue, 10 Dec 2019 21:38:42 +0530 Subject: [PATCH 67/76] fix-education: date of birth validation on student form (#19875) * fix: date validation on inpatient record, else condition removing on clinical prcd templ which is not req * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix: joining and relieving Date can be on same date as valid use case * fix-education: date of birth validation --- erpnext/education/doctype/student/student.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 76825cec1b2..8e4b4e16f9a 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -5,12 +5,14 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.utils import getdate,today from frappe import _ from frappe.desk.form.linked_with import get_linked_doctypes from erpnext.education.utils import check_content_completion, check_quiz_completion class Student(Document): def validate(self): self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) + self.validate_dates() if self.student_applicant: self.check_unique() @@ -19,6 +21,10 @@ class Student(Document): if frappe.get_value("Student", self.name, "title") != self.title: self.update_student_name_in_linked_doctype() + def validate_dates(self): + if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): + frappe.throw(_("Date of Birth cannot be greater than today.")) + def update_student_name_in_linked_doctype(self): linked_doctypes = get_linked_doctypes("Student") for d in linked_doctypes: From 9e32f587f5ab35142c4945ace04ba48718a32335 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Tue, 10 Dec 2019 17:11:05 +0100 Subject: [PATCH 68/76] fix: remove contact info, use international format (#19828) - for international letters, city and country should be upper case: https://www.deutschepost.de/content/dam/dpag/images/B_b/Briefe_ins_Ausland/downloads/dp-brief-international-handlingbroschuere-072019.pdf#page=15 - it is not customary, to use contact information such as phone number in the address --- erpnext/regional/germany/address_template.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/germany/address_template.html b/erpnext/regional/germany/address_template.html index 0df786713c9..7fa4c32612a 100644 --- a/erpnext/regional/germany/address_template.html +++ b/erpnext/regional/germany/address_template.html @@ -1,8 +1,8 @@ {{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%} -{{ pincode }} {{ city }}
-{% if country %}{{ country }}
{% endif -%} -
-{% if phone %}Tel: {{ phone }}
{% endif -%} -{% if fax %}Fax: {{ fax }}
{% endif -%} -{% if email_id %}E-Mail: {{ email_id }}
{% endif -%} +{% if country in ["Germany", "Deutschland"] %} + {{ pincode }} {{ city }} +{% else %} + {{ pincode }} {{ city | upper }}
+ {{ country | upper }} +{% endif %} From ca6dbad7cb8c4566cd811dd21162035b2279795f Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 11 Dec 2019 12:10:05 +0530 Subject: [PATCH 69/76] fix: Disable Rounded Total always showing field default value --- erpnext/public/js/controllers/buying.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 02c30587f67..926227b24c8 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -30,7 +30,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ && frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) { var df = frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total"); - var disable = df.default || cint(frappe.sys_defaults.disable_rounded_total); + var disable = cint(df.default) || cint(frappe.sys_defaults.disable_rounded_total); this.frm.set_value("disable_rounded_total", disable); } From 45e4b73e08e231862b264687d10849b3b9e4864e Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 11 Dec 2019 13:07:41 +0530 Subject: [PATCH 70/76] fix: rename fields --- .../quality_action_resolution/quality_action_resolution.json | 2 +- .../quality_procedure_process/quality_procedure_process.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json b/erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json index 74370cc3efe..a4e6aed86a0 100644 --- a/erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json +++ b/erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json @@ -13,7 +13,7 @@ "fieldname": "problem", "fieldtype": "Long Text", "in_list_view": 1, - "label": "Problem" + "label": "Review" }, { "fieldname": "sb_00", diff --git a/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json b/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json index f5c0fbc2523..0a67fa505ee 100644 --- a/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json +++ b/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json @@ -18,7 +18,7 @@ "fieldname": "procedure", "fieldtype": "Link", "in_list_view": 1, - "label": "Procedure", + "label": "Child Procedure", "options": "Quality Procedure" } ], From 37cf096102abf4b9520a7a78b4fa2867700d1488 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 11 Dec 2019 13:33:23 +0530 Subject: [PATCH 71/76] fix: not able to cancel the landed cost voucher --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d0fae6a2272..ad2f07d0846 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -609,7 +609,7 @@ def make_stock_entry(source_name,target_doc=None): def get_item_account_wise_additional_cost(purchase_document): landed_cost_voucher = frappe.get_value("Landed Cost Purchase Receipt", - {"receipt_document": purchase_document}, "parent") + {"receipt_document": purchase_document, "docstatus": 1}, "parent") if not landed_cost_voucher: return From 2ef26fbb0704b6ddd560708584625acca2e437ac Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 11 Dec 2019 14:21:04 +0530 Subject: [PATCH 72/76] fix: empty fname and fcontent of uploaded file --- .../doctype/bank_transaction/bank_transaction_upload.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py index deedafdfb5d..33ae45439e7 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py @@ -15,8 +15,8 @@ def upload_bank_statement(): with open(frappe.uploaded_file, "rb") as upfile: fcontent = upfile.read() else: - from frappe.utils.file_manager import get_uploaded_content - fname, fcontent = get_uploaded_content() + fcontent = frappe.local.uploaded_file + fname = frappe.local.uploaded_filename if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')): from frappe.utils.csvutils import read_csv_content From 9b0d7b93dd8c3e8a4d33a986fee256a8ee449699 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 12 Dec 2019 12:10:25 +0530 Subject: [PATCH 73/76] fix: not able to submit the landed cost voucher --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8e34a8a4797..060175f9045 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -245,7 +245,7 @@ class PurchaseReceipt(BuyingController): negative_expense_to_be_booked += flt(d.item_tax_amount) # Amount added through landed-cost-voucher - if landed_cost_entries: + if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): gl_entries.append(self.get_gl_dict({ "account": account, @@ -622,8 +622,7 @@ def get_item_account_wise_additional_cost(purchase_document): based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) for item in landed_cost_voucher_doc.items: - if item.receipt_document == purchase_document: - total_item_cost += item.get(based_on_field) + total_item_cost += item.get(based_on_field) for item in landed_cost_voucher_doc.items: if item.receipt_document == purchase_document: From c58dc873d501b38edbc41c6100e60b277fe04d84 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Thu, 12 Dec 2019 14:55:57 +0530 Subject: [PATCH 74/76] fix: Get regional address details fix --- erpnext/regional/india/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 77bcc80abab..0f9156a6b4c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -179,6 +179,8 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N if not party_details.place_of_supply: return + if not party_details.company_gstin: return + if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): From 0e33f792d3aae99c781e804b5fc27ceb497ad5eb Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 12 Dec 2019 16:34:41 +0530 Subject: [PATCH 75/76] fix: Distribute charges based on quantity if Total Basic Amount is Zero. --- .../stock/doctype/stock_entry/stock_entry.py | 12 +++--- .../doctype/stock_entry/test_stock_entry.py | 39 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 913656ad020..00d27ef232b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -27,7 +27,6 @@ class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass -class TotalBasicAmountZeroError(frappe.ValidationError): pass from erpnext.controllers.stock_controller import StockController @@ -650,11 +649,11 @@ class StockEntry(StockController): gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse]) + divide_based_on = total_basic_amount if self.get("additional_costs") and not total_basic_amount: - #If additional costs table is populated and total basic amount is - #somehow 0, interrupt transaction. - frappe.throw(_("Total Basic Amount in Items Table cannot be 0"), TotalBasicAmountZeroError) + # if total_basic_amount is 0, distribute additional charges based on qty + divide_based_on = sum(item.qty for item in list(self.get("items"))) item_account_wise_additional_cost = {} @@ -663,8 +662,11 @@ class StockEntry(StockController): if d.t_warehouse: item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) + + multiply_based_on = d.basic_amount if total_basic_amount else d.qty + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ - (t.amount * d.basic_amount) / total_basic_amount + (t.amount * multiply_based_on) / divide_based_on if item_account_wise_additional_cost: for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c5e67092d3d..ee5f2370987 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -16,7 +16,6 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError -from erpnext.stock.doctype.stock_entry.stock_entry import TotalBasicAmountZeroError from six import iteritems def get_sle(**args): @@ -798,14 +797,26 @@ class TestStockEntry(unittest.TestCase): "posting_date": nowdate(), "company":"_Test Company with perpetual inventory", "items":[ - {"item_code":"Basil Leaves", - "description":"Basil Leaves", - "qty": 1, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1"} + { + "item_code":"Basil Leaves", + "description":"Basil Leaves", + "qty": 1, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, + { + "item_code":"Basil Leaves", + "description":"Basil Leaves", + "qty": 2, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, ], "additional_costs":[ {"expense_account":"Miscellaneous Expenses - TCP1", @@ -813,9 +824,15 @@ class TestStockEntry(unittest.TestCase): "description": "miscellanous"} ] }) - se.insert() - self.assertRaises(TotalBasicAmountZeroError, se.submit) + se.submit() + + self.check_gl_entries("Stock Entry", se.name, + sorted([ + ["Stock Adjustment - TCP1", 100.0, 0.0], + ["Miscellaneous Expenses - TCP1", 0.0, 100.0] + ]) + ) def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) From 8c9c6ec919570ce73432286b563e2f930b4f3212 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 13 Dec 2019 13:47:15 +0530 Subject: [PATCH 76/76] fix: Price rule filtering fix --- erpnext/accounts/doctype/pricing_rule/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 637e503e658..7af6748254f 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -284,7 +284,7 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None): status = True # if user has created item price against the transaction UOM - if rule.get("uom") == args.get("uom"): + if args and rule.get("uom") == args.get("uom"): conversion_factor = 1.0 if status and (flt(rate) >= (flt(rule.min_amt) * conversion_factor)