From 73f969fd7ffc1f75145a68c1bfddca0a0205df56 Mon Sep 17 00:00:00 2001 From: Ameya Shenoy Date: Fri, 21 Jul 2017 14:22:08 +0530 Subject: [PATCH 01/31] [ui-test] manufacturing item creation testing (#10009) --- erpnext/crm/doctype/item/test_item.js | 85 +++++++++++++++++++++++++++ erpnext/tests/ui/tests.txt | 3 +- 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 erpnext/crm/doctype/item/test_item.js diff --git a/erpnext/crm/doctype/item/test_item.js b/erpnext/crm/doctype/item/test_item.js new file mode 100644 index 0000000000..a490b94b54 --- /dev/null +++ b/erpnext/crm/doctype/item/test_item.js @@ -0,0 +1,85 @@ +QUnit.test("test: item", function (assert) { + assert.expect(18); + let done = assert.async(); + frappe.run_serially([ + // test item creation + () => frappe.set_route("List", "Item"), + + // Create a keyboard item + () => frappe.tests.make( + "Item", [ + {item_code: "Keyboard"}, + {item_group: "Products"}, + {is_stock_item: 1}, + {standard_rate: 1000}, + {opening_stock: 100} + ] + ), + () => { + assert.ok(cur_frm.doc.item_name.includes('Keyboard'), + 'Item Keyboard created correctly'); + assert.ok(cur_frm.doc.item_code.includes('Keyboard'), + 'item_code for Keyboard set correctly'); + assert.ok(cur_frm.doc.item_group.includes('Products'), + 'item_group for Keyboard set correctly'); + assert.equal(cur_frm.doc.is_stock_item, 1, + 'is_stock_item for Keyboard set correctly'); + assert.equal(cur_frm.doc.standard_rate, 1000, + 'standard_rate for Keyboard set correctly'); + assert.equal(cur_frm.doc.opening_stock, 100, + 'opening_stock for Keyboard set correctly'); + }, + + // Create a Screen item + () => frappe.tests.make( + "Item", [ + {item_code: "Screen"}, + {item_group: "Products"}, + {is_stock_item: 1}, + {standard_rate: 1000}, + {opening_stock: 100} + ] + ), + () => { + assert.ok(cur_frm.doc.item_name.includes('Screen'), + 'Item Screen created correctly'); + assert.ok(cur_frm.doc.item_code.includes('Screen'), + 'item_code for Screen set correctly'); + assert.ok(cur_frm.doc.item_group.includes('Products'), + 'item_group for Screen set correctly'); + assert.equal(cur_frm.doc.is_stock_item, 1, + 'is_stock_item for Screen set correctly'); + assert.equal(cur_frm.doc.standard_rate, 1000, + 'standard_rate for Screen set correctly'); + assert.equal(cur_frm.doc.opening_stock, 100, + 'opening_stock for Screen set correctly'); + }, + + // Create a CPU item + () => frappe.tests.make( + "Item", [ + {item_code: "CPU"}, + {item_group: "Products"}, + {is_stock_item: 1}, + {standard_rate: 1000}, + {opening_stock: 100} + ] + ), + () => { + assert.ok(cur_frm.doc.item_name.includes('CPU'), + 'Item CPU created correctly'); + assert.ok(cur_frm.doc.item_code.includes('CPU'), + 'item_code for CPU set correctly'); + assert.ok(cur_frm.doc.item_group.includes('Products'), + 'item_group for CPU set correctly'); + assert.equal(cur_frm.doc.is_stock_item, 1, + 'is_stock_item for CPU set correctly'); + assert.equal(cur_frm.doc.standard_rate, 1000, + 'standard_rate for CPU set correctly'); + assert.equal(cur_frm.doc.opening_stock, 100, + 'opening_stock for CPU set correctly'); + }, + + () => done() + ]); +}); diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index 42233dc800..98f83999c9 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -2,4 +2,5 @@ erpnext/tests/ui/make_fixtures.js #long erpnext/accounts/doctype/account/test_account.js erpnext/crm/doctype/lead/test_lead.js erpnext/crm/doctype/opportunity/test_opportunity.js -erpnext/selling/doctype/quotation/test_quotation.js \ No newline at end of file +erpnext/selling/doctype/quotation/test_quotation.js +erpnext/crm/doctype/item/test_item.js \ No newline at end of file From 9306aff1bb4024cacfd6c25249ad34ec0e5464ba Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Fri, 21 Jul 2017 15:19:47 +0530 Subject: [PATCH 02/31] [minor] moved the patch to v8_5 and other minor fixes (#10012) * Quotation and Supplier Quotation Route and Permission Edits * [minor] moved the patch to v8_5 and other minor fixes --- erpnext/hooks.py | 17 ++++++++++++----- erpnext/patches.txt | 3 ++- .../v8_5/remove_quotations_route_in_sidebar.py | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 erpnext/patches/v8_5/remove_quotations_route_in_sidebar.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 472d8f9a9e..5f0b19ebd7 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -72,14 +72,20 @@ website_route_rules = [ "parents": [{"title": _("Invoices"), "name": "invoices"}] } }, - {"from_route": "/quotations", "to_route": "Supplier Quotation"}, - {"from_route": "/quotations/", "to_route": "order", + {"from_route": "/supplier-quotations", "to_route": "Supplier Quotation"}, + {"from_route": "/supplier-quotations/", "to_route": "order", "defaults": { "doctype": "Supplier Quotation", "parents": [{"title": _("Supplier Quotation"), "name": "quotations"}] } }, - {"from_route": "/quotation", "to_route": "Quotation"}, + {"from_route": "/quotations", "to_route": "Quotation"}, + {"from_route": "/quotations/", "to_route": "order", + "defaults": { + "doctype": "Quotation", + "parents": [{"title": _("Quotations"), "name": "quotation"}] + } + }, {"from_route": "/shipments", "to_route": "Delivery Note"}, {"from_route": "/shipments/", "to_route": "order", "defaults": { @@ -109,8 +115,8 @@ website_route_rules = [ standard_portal_menu_items = [ {"title": _("Projects"), "route": "/project", "reference_doctype": "Project"}, {"title": _("Request for Quotations"), "route": "/rfq", "reference_doctype": "Request for Quotation", "role": "Supplier"}, - {"title": _("Supplier Quotation"), "route": "/quotations", "reference_doctype": "Supplier Quotation", "role": "Supplier"}, - {"title": _("Quotations"), "route": "/quotation", "reference_doctype": "Quotation", "role":"Customer"}, + {"title": _("Supplier Quotation"), "route": "/supplier-quotations", "reference_doctype": "Supplier Quotation", "role": "Supplier"}, + {"title": _("Quotations"), "route": "/quotations", "reference_doctype": "Quotation", "role":"Customer"}, {"title": _("Orders"), "route": "/orders", "reference_doctype": "Sales Order", "role":"Customer"}, {"title": _("Invoices"), "route": "/invoices", "reference_doctype": "Sales Invoice", "role":"Customer"}, {"title": _("Shipments"), "route": "/shipments", "reference_doctype": "Delivery Note", "role":"Customer"}, @@ -127,6 +133,7 @@ default_roles = [ has_website_permission = { "Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission", + "Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission", "Sales Invoice": "erpnext.controllers.website_list_for_contact.has_website_permission", "Supplier Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission", "Delivery Note": "erpnext.controllers.website_list_for_contact.has_website_permission", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fb8f02e94b..834ed2fdf4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -423,4 +423,5 @@ erpnext.patches.v8_3.set_restrict_to_domain_for_module_def erpnext.patches.v8_1.update_expense_claim_status erpnext.patches.v8_3.update_company_total_sales erpnext.patches.v8_1.set_delivery_date_in_so_item -erpnext.patches.v8_5.fix_tax_breakup_for_non_invoice_docs \ No newline at end of file +erpnext.patches.v8_5.fix_tax_breakup_for_non_invoice_docs +erpnext.patches.v8_5.remove_quotations_route_in_sidebar \ No newline at end of file diff --git a/erpnext/patches/v8_5/remove_quotations_route_in_sidebar.py b/erpnext/patches/v8_5/remove_quotations_route_in_sidebar.py new file mode 100644 index 0000000000..2d7df4a179 --- /dev/null +++ b/erpnext/patches/v8_5/remove_quotations_route_in_sidebar.py @@ -0,0 +1,16 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doctype("Portal Settings") + + frappe.db.sql(""" + delete from + `tabPortal Menu Item` + where + (route = '/quotations' and title = 'Supplier Quotation') + or (route = '/quotation' and title = 'Quotations') + """) \ No newline at end of file From 65656ec2df789ee86331bb56fe2ad9a8824b8159 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 24 Jul 2017 07:02:26 +0100 Subject: [PATCH 03/31] hide salution and gender if company type is Company (#10040) --- erpnext/selling/doctype/customer/customer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 1b77418bd2..52c6b6db86 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -81,6 +81,7 @@ "bold": 0, "collapsible": 0, "columns": 0, + "depends_on": "eval:doc.customer_type!='Company'", "fieldname": "salutation", "fieldtype": "Link", "hidden": 0, @@ -143,6 +144,7 @@ "bold": 0, "collapsible": 0, "columns": 0, + "depends_on": "eval:doc.customer_type != 'Company'", "fieldname": "gender", "fieldtype": "Link", "hidden": 0, @@ -1200,7 +1202,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-28 14:55:39.910819", + "modified": "2017-07-24 00:55:07.445783", "modified_by": "Administrator", "module": "Selling", "name": "Customer", From 20a862a6b98c08b46e90ed4383a7fc5479f2cc61 Mon Sep 17 00:00:00 2001 From: Utkarsh Yadav Date: Mon, 24 Jul 2017 14:33:42 +0530 Subject: [PATCH 04/31] [ui test] holiday list and branch in HR (#10045) * added test for holiday list * codacy fixes * added check for all days in list * added test for branch * codacy fixe * minor fixes --- erpnext/hr/doctype/branch/test_branch.js | 22 ++++++++++ .../doctype/holiday_list/test_holiday_list.js | 42 +++++++++++++++++++ erpnext/tests/ui/tests.txt | 4 +- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 erpnext/hr/doctype/branch/test_branch.js create mode 100644 erpnext/hr/doctype/holiday_list/test_holiday_list.js diff --git a/erpnext/hr/doctype/branch/test_branch.js b/erpnext/hr/doctype/branch/test_branch.js new file mode 100644 index 0000000000..25ea2fc392 --- /dev/null +++ b/erpnext/hr/doctype/branch/test_branch.js @@ -0,0 +1,22 @@ +QUnit.module('hr'); + +QUnit.test("Test: Branch [HR]", function (assert) { + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + // test branch creation + () => frappe.set_route("List", "Branch", "List"), + () => frappe.new_doc("Branch"), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("branch", "Branch test"), + + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + () => assert.equal("Branch test", cur_frm.doc.branch, + 'name of branch correctly saved'), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.js b/erpnext/hr/doctype/holiday_list/test_holiday_list.js new file mode 100644 index 0000000000..ed75285a65 --- /dev/null +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.js @@ -0,0 +1,42 @@ +QUnit.module('hr'); + +QUnit.test("Test: Holiday list [HR]", function (assert) { + assert.expect(3); + let done = assert.async(); + let date = frappe.datetime.add_months(frappe.datetime.nowdate(), -2); // date 2 months from now + + frappe.run_serially([ + // test holiday list creation + () => frappe.set_route("List", "Holiday List", "List"), + () => frappe.new_doc("Holiday List"), + () => frappe.timeout(1), + () => cur_frm.set_value("holiday_list_name", "Holiday list test"), + () => cur_frm.set_value("from_date", date), + () => cur_frm.set_value("weekly_off", "Sunday"), // holiday list for sundays + () => frappe.click_button('Get Weekly Off Dates'), + + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + () => assert.equal("Holiday list test", cur_frm.doc.holiday_list_name, + 'name of holiday list correctly saved'), + + // check if holiday list contains correct days + () => { + var list = cur_frm.doc.holidays; + var list_length = list.length; + var i = 0; + for ( ; i < list_length; i++) + if (list[i].description != 'Sunday') break; + assert.equal(list_length, i, "all holidays are sundays in holiday list"); + }, + + // check if to_date is set one year from from_date + () => { + var date_year_later = frappe.datetime.add_days(frappe.datetime.add_months(date, 12), -1); // date after one year + assert.equal(date_year_later, cur_frm.doc.to_date, + "to date set correctly"); + }, + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index 98f83999c9..c149226628 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -3,4 +3,6 @@ erpnext/accounts/doctype/account/test_account.js erpnext/crm/doctype/lead/test_lead.js erpnext/crm/doctype/opportunity/test_opportunity.js erpnext/selling/doctype/quotation/test_quotation.js -erpnext/crm/doctype/item/test_item.js \ No newline at end of file +erpnext/crm/doctype/item/test_item.js +erpnext/hr/doctype/holiday_list/test_holiday_list.js +erpnext/hr/doctype/branch/test_branch.js \ No newline at end of file From 91b28337086026c7348b3660f9cfadd7548541d8 Mon Sep 17 00:00:00 2001 From: Ameya Shenoy Date: Mon, 24 Jul 2017 14:34:30 +0530 Subject: [PATCH 05/31] [ui-tests] added workstation and operation (#10044) * [ui-test] workstation and operation testing added * [ui-tests] removed unnecessary assertions and used logical names for operations and workstations --- erpnext/crm/doctype/item/test_item.js | 79 ++++++++-------- .../doctype/operation/test_operation.js | 65 ++++++++++++++ .../doctype/workstation/test_workstation.js | 89 +++++++++++++++++++ erpnext/tests/ui/tests.txt | 4 +- 4 files changed, 195 insertions(+), 42 deletions(-) create mode 100644 erpnext/manufacturing/doctype/operation/test_operation.js create mode 100644 erpnext/manufacturing/doctype/workstation/test_workstation.js diff --git a/erpnext/crm/doctype/item/test_item.js b/erpnext/crm/doctype/item/test_item.js index a490b94b54..2f50ebedc3 100644 --- a/erpnext/crm/doctype/item/test_item.js +++ b/erpnext/crm/doctype/item/test_item.js @@ -1,6 +1,12 @@ QUnit.test("test: item", function (assert) { - assert.expect(18); + assert.expect(6); let done = assert.async(); + let keyboard_cost = 800; + let screen_cost = 4000; + let CPU_cost = 15000; + let scrap_cost = 100; + let no_of_items_to_stock = 100; + let is_stock_item = 1; frappe.run_serially([ // test item creation () => frappe.set_route("List", "Item"), @@ -10,9 +16,9 @@ QUnit.test("test: item", function (assert) { "Item", [ {item_code: "Keyboard"}, {item_group: "Products"}, - {is_stock_item: 1}, - {standard_rate: 1000}, - {opening_stock: 100} + {is_stock_item: is_stock_item}, + {standard_rate: keyboard_cost}, + {opening_stock: no_of_items_to_stock} ] ), () => { @@ -22,11 +28,11 @@ QUnit.test("test: item", function (assert) { 'item_code for Keyboard set correctly'); assert.ok(cur_frm.doc.item_group.includes('Products'), 'item_group for Keyboard set correctly'); - assert.equal(cur_frm.doc.is_stock_item, 1, + assert.equal(cur_frm.doc.is_stock_item, is_stock_item, 'is_stock_item for Keyboard set correctly'); - assert.equal(cur_frm.doc.standard_rate, 1000, + assert.equal(cur_frm.doc.standard_rate, keyboard_cost, 'standard_rate for Keyboard set correctly'); - assert.equal(cur_frm.doc.opening_stock, 100, + assert.equal(cur_frm.doc.opening_stock, no_of_items_to_stock, 'opening_stock for Keyboard set correctly'); }, @@ -35,50 +41,41 @@ QUnit.test("test: item", function (assert) { "Item", [ {item_code: "Screen"}, {item_group: "Products"}, - {is_stock_item: 1}, - {standard_rate: 1000}, - {opening_stock: 100} + {is_stock_item: is_stock_item}, + {standard_rate: screen_cost}, + {opening_stock: no_of_items_to_stock} ] ), - () => { - assert.ok(cur_frm.doc.item_name.includes('Screen'), - 'Item Screen created correctly'); - assert.ok(cur_frm.doc.item_code.includes('Screen'), - 'item_code for Screen set correctly'); - assert.ok(cur_frm.doc.item_group.includes('Products'), - 'item_group for Screen set correctly'); - assert.equal(cur_frm.doc.is_stock_item, 1, - 'is_stock_item for Screen set correctly'); - assert.equal(cur_frm.doc.standard_rate, 1000, - 'standard_rate for Screen set correctly'); - assert.equal(cur_frm.doc.opening_stock, 100, - 'opening_stock for Screen set correctly'); - }, // Create a CPU item () => frappe.tests.make( "Item", [ {item_code: "CPU"}, {item_group: "Products"}, - {is_stock_item: 1}, - {standard_rate: 1000}, - {opening_stock: 100} + {is_stock_item: is_stock_item}, + {standard_rate: CPU_cost}, + {opening_stock: no_of_items_to_stock} + ] + ), + + // Create a laptop item + () => frappe.tests.make( + "Item", [ + {item_code: "Laptop"}, + {item_group: "Products"} + ] + ), + + // Create a scrap item + () => frappe.tests.make( + "Item", [ + {item_code: "Scrap item"}, + {item_group: "Products"}, + {is_stock_item: is_stock_item}, + {standard_rate: scrap_cost}, + {opening_stock: no_of_items_to_stock} ] ), - () => { - assert.ok(cur_frm.doc.item_name.includes('CPU'), - 'Item CPU created correctly'); - assert.ok(cur_frm.doc.item_code.includes('CPU'), - 'item_code for CPU set correctly'); - assert.ok(cur_frm.doc.item_group.includes('Products'), - 'item_group for CPU set correctly'); - assert.equal(cur_frm.doc.is_stock_item, 1, - 'is_stock_item for CPU set correctly'); - assert.equal(cur_frm.doc.standard_rate, 1000, - 'standard_rate for CPU set correctly'); - assert.equal(cur_frm.doc.opening_stock, 100, - 'opening_stock for CPU set correctly'); - }, () => done() ]); diff --git a/erpnext/manufacturing/doctype/operation/test_operation.js b/erpnext/manufacturing/doctype/operation/test_operation.js new file mode 100644 index 0000000000..9fedaac720 --- /dev/null +++ b/erpnext/manufacturing/doctype/operation/test_operation.js @@ -0,0 +1,65 @@ +QUnit.test("test: operation", function (assert) { + assert.expect(2); + let done = assert.async(); + let set_op_name = (text) => { + $(`input.input-with-feedback.form-control.bold:visible`).val(`${text}`); + }; + let click_create = () => { + $(`.btn-primary:contains("Create"):visible`).click(); + }; + + frappe.run_serially([ + // test operation creation + () => frappe.set_route("List", "Operation"), + + // Create a Keyboard operation + () => { + frappe.tests.make( + "Operation", [ + {workstation: "Keyboard assembly workstation"} + ] + ); + }, + () => frappe.timeout(4), + () => set_op_name("Assemble Keyboard"), + () => frappe.timeout(0.5), + () => click_create(), + () => frappe.timeout(1), + () => { + assert.ok(cur_frm.docname.includes('Assemble Keyboard'), + 'Assemble Keyboard created successfully'); + assert.ok(cur_frm.doc.workstation.includes('Keyboard assembly workstation'), + 'Keyboard assembly workstation was linked successfully'); + }, + + // Create a Screen operation + () => { + frappe.tests.make( + "Operation", [ + {workstation: "Screen assembly workstation"} + ] + ); + }, + () => frappe.timeout(4), + () => set_op_name("Assemble Screen"), + () => frappe.timeout(0.5), + () => click_create(), + () => frappe.timeout(1), + + // Create a CPU operation + () => { + frappe.tests.make( + "Operation", [ + {workstation: "CPU assembly workstation"} + ] + ); + }, + () => frappe.timeout(4), + () => set_op_name("Assemble CPU"), + () => frappe.timeout(0.5), + () => click_create(), + () => frappe.timeout(1), + + () => done() + ]); +}); diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.js b/erpnext/manufacturing/doctype/workstation/test_workstation.js new file mode 100644 index 0000000000..1df53d058f --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.js @@ -0,0 +1,89 @@ +QUnit.test("test: workstation", function (assert) { + assert.expect(9); + let done = assert.async(); + let elec_rate = 50; + let rent = 100; + let consumable_rate = 20; + let labour_rate = 500; + frappe.run_serially([ + // test workstation creation + () => frappe.set_route("List", "Workstation"), + + // Create a keyboard workstation + () => frappe.tests.make( + "Workstation", [ + {workstation_name: "Keyboard assembly workstation"}, + {hour_rate_electricity: elec_rate}, + {hour_rate_rent: rent}, + {hour_rate_consumable: consumable_rate}, + {hour_rate_labour: labour_rate}, + {working_hours: [ + [ + {enabled: 1}, + {start_time: '11:00:00'}, + {end_time: '18:00:00'} + ] + ]} + ] + ), + () => { + assert.ok(cur_frm.doc.workstation_name.includes('Keyboard assembly workstation'), + 'Keyboard assembly workstation created successfully'); + assert.equal(cur_frm.doc.hour_rate_electricity, elec_rate, + 'electricity rate set correctly'); + assert.equal(cur_frm.doc.hour_rate_rent, rent, + 'rent set correctly'); + assert.equal(cur_frm.doc.hour_rate_consumable, consumable_rate, + 'consumable rate set correctly'); + assert.equal(cur_frm.doc.hour_rate_labour, labour_rate, + 'labour rate set correctly'); + assert.equal(cur_frm.doc.working_hours[0].enabled, 1, + 'working hours enabled'); + assert.ok(cur_frm.doc.working_hours[0].start_time.includes('11:00:0'), + 'start time set correctly'); + assert.ok(cur_frm.doc.working_hours[0].end_time.includes('18:00:0'), + 'end time set correctly'); + assert.ok(cur_frm.doc.hour_rate_electricity+cur_frm.doc.hour_rate_rent+ + cur_frm.doc.hour_rate_consumable+cur_frm.doc.hour_rate_labour== + cur_frm.doc.hour_rate, 'Net hour rate set correctly'); + }, + + // Create a Screen workstation + () => frappe.tests.make( + "Workstation", [ + {workstation_name: "Screen assembly workstation"}, + {hour_rate_electricity: elec_rate}, + {hour_rate_rent: rent}, + {hour_rate_consumable: consumable_rate}, + {hour_rate_labour: labour_rate}, + {working_hours: [ + [ + {enabled: 1}, + {start_time: '11:00:00'}, + {end_time: '18:00:00'} + ] + ]} + ] + ), + + // Create a CPU workstation + () => frappe.tests.make( + "Workstation", [ + {workstation_name: "CPU assembly workstation"}, + {hour_rate_electricity: elec_rate}, + {hour_rate_rent: rent}, + {hour_rate_consumable: consumable_rate}, + {hour_rate_labour: labour_rate}, + {working_hours: [ + [ + {enabled: 1}, + {start_time: '11:00:00'}, + {end_time: '18:00:00'} + ] + ]} + ] + ), + + () => done() + ]); +}); diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index c149226628..f7e495625f 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -4,5 +4,7 @@ erpnext/crm/doctype/lead/test_lead.js erpnext/crm/doctype/opportunity/test_opportunity.js erpnext/selling/doctype/quotation/test_quotation.js erpnext/crm/doctype/item/test_item.js +erpnext/manufacturing/doctype/workstation/test_workstation.js +erpnext/manufacturing/doctype/operation/test_operation.js erpnext/hr/doctype/holiday_list/test_holiday_list.js -erpnext/hr/doctype/branch/test_branch.js \ No newline at end of file +erpnext/hr/doctype/branch/test_branch.js From 96381da547665f92dd0e1d4a7cbcca68f3d60e33 Mon Sep 17 00:00:00 2001 From: bcornwellmott Date: Mon, 24 Jul 2017 10:12:30 -0700 Subject: [PATCH 06/31] Supplier Scorecard (#9294) * Initial start of scorecard docs * Got basic functionality working * Fixed doc names and added key functions * Basic functional version minus Actions * Hide scorecard docs until functional * Created supplier scorecard documentation * Added default variables and standings. Added restrictions for PO + RFQ * Automatic daily scorecard creation + on save * Added warning for PO nd RFQs * Minor fixes for codepy, automatically add variables for criteria, fix hooks.py typo * Added tests, fixed codacy formatting, small improvements * Fixed test bug w/ criteria. Codacy cleanup * Fixed codacy issues. Fixed sticky criteria * Fixed bug with period search. Remove blank variable child. * Updated docs, automatically add criteria and standings, clean up period create message * Uncommented test, set docs to beta * Fix for nabinhait review * Fix codacy issue. Fix dict assignment for records --- erpnext/buying/__init__.py | 1 + .../doctype/purchase_order/purchase_order.py | 13 + .../request_for_quotation.py | 12 + erpnext/buying/doctype/supplier/supplier.json | 122 ++- .../doctype/supplier_scorecard/__init__.py | 0 .../supplier_scorecard/supplier_scorecard.js | 146 ++++ .../supplier_scorecard.json | 701 ++++++++++++++++++ .../supplier_scorecard/supplier_scorecard.py | 262 +++++++ .../supplier_scorecard_dashboard.py | 15 + .../supplier_scorecard_list.js | 17 + .../test_supplier_scorecard.py | 190 +++++ .../supplier_scorecard_criteria/__init__.py | 0 .../supplier_scorecard_criteria.js | 8 + .../supplier_scorecard_criteria.json | 184 +++++ .../supplier_scorecard_criteria.py | 89 +++ .../test_supplier_scorecard_criteria.py | 75 ++ .../supplier_scorecard_period/__init__.py | 0 .../supplier_scorecard_period.js | 14 + .../supplier_scorecard_period.json | 397 ++++++++++ .../supplier_scorecard_period.py | 133 ++++ .../test_supplier_scorecard_period.py | 9 + .../__init__.py | 0 .../supplier_scorecard_scoring_criteria.json | 280 +++++++ .../supplier_scorecard_scoring_criteria.py | 9 + .../__init__.py | 0 .../supplier_scorecard_scoring_standing.json | 491 ++++++++++++ .../supplier_scorecard_scoring_standing.py | 9 + .../__init__.py | 0 .../supplier_scorecard_scoring_variable.json | 222 ++++++ .../supplier_scorecard_scoring_variable.py | 9 + .../supplier_scorecard_standing/__init__.py | 0 .../supplier_scorecard_standing.js | 10 + .../supplier_scorecard_standing.json | 424 +++++++++++ .../supplier_scorecard_standing.py | 29 + .../test_supplier_scorecard_standing.py | 9 + .../supplier_scorecard_variable/__init__.py | 0 .../supplier_scorecard_variable.js | 10 + .../supplier_scorecard_variable.json | 242 ++++++ .../supplier_scorecard_variable.py | 503 +++++++++++++ .../test_supplier_scorecard_variable.py | 57 ++ erpnext/config/buying.py | 26 + .../buying/supplier-scorecard-criteria.png | Bin 0 -> 22343 bytes .../buying/supplier-scorecard-standing.png | Bin 0 -> 20653 bytes .../buying/supplier-scorecard-weighing.png | Bin 0 -> 23290 bytes .../assets/img/buying/supplier-scorecard.png | Bin 0 -> 54354 bytes erpnext/docs/user/manual/en/buying/index.txt | 1 + .../manual/en/buying/supplier-scorecard.md | 76 ++ erpnext/hooks.py | 1 + erpnext/patches.txt | 3 +- erpnext/patches/v8_4/__init__.py | 1 + .../patches/v8_4/make_scorecard_records.py | 9 + .../setup/setup_wizard/install_fixtures.py | 4 + 52 files changed, 4811 insertions(+), 2 deletions(-) create mode 100644 erpnext/buying/doctype/supplier_scorecard/__init__.py create mode 100644 erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js create mode 100644 erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.json create mode 100644 erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py create mode 100644 erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py create mode 100644 erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js create mode 100644 erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_criteria/__init__.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js create mode 100644 erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.json create mode 100644 erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_period/__init__.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js create mode 100644 erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.json create mode 100644 erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_period/test_supplier_scorecard_period.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_criteria/__init__.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_criteria/supplier_scorecard_scoring_criteria.json create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_criteria/supplier_scorecard_scoring_criteria.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_standing/__init__.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_standing/supplier_scorecard_scoring_standing.json create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_standing/supplier_scorecard_scoring_standing.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_variable/__init__.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.json create mode 100644 erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_standing/__init__.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js create mode 100644 erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.json create mode 100644 erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_standing/test_supplier_scorecard_standing.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_variable/__init__.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js create mode 100644 erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.json create mode 100644 erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py create mode 100644 erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py create mode 100644 erpnext/docs/assets/img/buying/supplier-scorecard-criteria.png create mode 100644 erpnext/docs/assets/img/buying/supplier-scorecard-standing.png create mode 100644 erpnext/docs/assets/img/buying/supplier-scorecard-weighing.png create mode 100644 erpnext/docs/assets/img/buying/supplier-scorecard.png create mode 100644 erpnext/docs/user/manual/en/buying/supplier-scorecard.md create mode 100644 erpnext/patches/v8_4/__init__.py create mode 100644 erpnext/patches/v8_4/make_scorecard_records.py diff --git a/erpnext/buying/__init__.py b/erpnext/buying/__init__.py index e69de29bb2..baffc48825 100644 --- a/erpnext/buying/__init__.py +++ b/erpnext/buying/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 7e5020a08d..26c8c61167 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -39,6 +39,8 @@ class PurchaseOrder(BuyingController): super(PurchaseOrder, self).validate() self.set_status() + + self.validate_supplier() validate_for_items(self) self.check_for_closed_status() @@ -65,6 +67,17 @@ class PurchaseOrder(BuyingController): } }) + def validate_supplier(self): + prevent_po = frappe.db.get_value("Supplier", self.supplier, 'prevent_pos') + if prevent_po: + standing = frappe.db.get_value("Supplier Scorecard",self.supplier, 'status') + frappe.throw(_("Purchase Orders are not allowed for {0} due to a scorecard standing of {1}.").format(self.supplier, standing)) + + warn_po = frappe.db.get_value("Supplier", self.supplier, 'warn_pos') + if warn_po: + standing = frappe.db.get_value("Supplier Scorecard",self.supplier, 'status') + frappe.msgprint(_("{0} currently has a {1} Supplier Scorecard standing, and Purchase Orders to this supplier should be issued with caution.").format(self.supplier, standing), title=_("Caution"), indicator='orange') + def validate_minimum_order_qty(self): items = list(set([d.item_code for d in self.get("items")])) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 9109239e93..666a1c6e8a 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -21,6 +21,7 @@ STANDARD_USERS = ("Guest", "Administrator") class RequestforQuotation(BuyingController): def validate(self): self.validate_duplicate_supplier() + self.validate_supplier_list() validate_for_items(self) self.update_email_id() @@ -29,6 +30,17 @@ class RequestforQuotation(BuyingController): if len(supplier_list) != len(set(supplier_list)): frappe.throw(_("Same supplier has been entered multiple times")) + def validate_supplier_list(self): + for d in self.suppliers: + prevent_rfqs = frappe.db.get_value("Supplier", d.supplier, 'prevent_rfqs') + if prevent_rfqs: + standing = frappe.db.get_value("Supplier Scorecard",d.supplier, 'status') + frappe.throw(_("RFQs are not allowed for {0} due to a scorecard standing of {1}").format(d.supplier, standing)) + warn_rfqs = frappe.db.get_value("Supplier", d.supplier, 'warn_rfqs') + if warn_rfqs: + standing = frappe.db.get_value("Supplier Scorecard",d.supplier, 'status') + frappe.msgprint(_("{0} currently has a {1} Supplier Scorecard standing, and RFQs to this supplier should be issued with caution.").format(d.supplier, standing), title=_("Caution"), indicator='orange') + def update_email_id(self): for rfq_supplier in self.suppliers: if not rfq_supplier.email_id: diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 58a1fc7464..711e05d913 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -322,6 +322,126 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warn_rfqs", + "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": "Warn RFQs", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warn_pos", + "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": "Warn POs", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "prevent_rfqs", + "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": "Prevent RFQs", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "prevent_pos", + "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": "Prevent POs", + "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, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -850,7 +970,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-13 14:29:16.310834", + "modified": "2017-07-06 16:40:46.935608", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier_scorecard/__init__.py b/erpnext/buying/doctype/supplier_scorecard/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js new file mode 100644 index 0000000000..a3a14147f2 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js @@ -0,0 +1,146 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +/* global frappe, refresh_field */ + +frappe.ui.form.on("Supplier Scorecard", { + + onload: function(frm) { + + if (frm.doc.indicator_color !== "") { + frm.set_indicator_formatter("status", function(doc) { + return doc.indicator_color.toLowerCase(); + }); + } + if (frm.doc.__unsaved == 1) { + loadAllCriteria(frm); + loadAllStandings(frm); + } + + }, + refresh: function(frm) { + if (frm.dashboard.hasOwnProperty('heatmap')) { + frm.dashboard.heatmap.setLegend([0,20,40,60,80,101],["#991600","#169900"]); + } + } + +}); + +frappe.ui.form.on("Supplier Scorecard Scoring Standing", { + + standing_name: function(frm, cdt, cdn) { + if (frm.doc.standing_name != undefined) { + var d = frappe.get_doc(cdt, cdn); + return frm.call({ + method: "erpnext.buying.doctype.supplier_scorecard_standing.supplier_scorecard_standing.get_scoring_standing", + child: d, + args: { + standing_name: d.standing_name + } + }); + } + } +}); + +frappe.ui.form.on("Supplier Scorecard Scoring Variable", { + + variable_label: function(frm, cdt, cdn) { + if (frm.doc.variable_label != undefined) { + var d = frappe.get_doc(cdt, cdn); + return frm.call({ + method: "erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable.get_scoring_variable", + child: d, + args: { + variable_label: d.variable_label + } + }); + } + } +}); + +frappe.ui.form.on("Supplier Scorecard Scoring Criteria", { + + criteria_name: function(frm, cdt, cdn) { + if (frm.doc.criteria_name != undefined) { + var d = frappe.get_doc(cdt, cdn); + frm.call({ + method: "erpnext.buying.doctype.supplier_scorecard_criteria.supplier_scorecard_criteria.get_variables", + args: { + criteria_name: d.criteria_name + }, + callback: function(r) { + for (var i = 0; i < r.message.length; i++) + { + var exists = false; + for (var j = 0; j < frm.doc.variables.length; j++) + { + if(!frm.doc.variables[j].hasOwnProperty("variable_label")) { + frm.get_field("variables").grid.grid_rows[j].remove(); + } + else if(frm.doc.variables[j].variable_label === r.message[i]) { + exists = true; + } + } + if (!exists){ + var new_row = frm.add_child("variables"); + new_row.variable_label = r.message[i]; + frm.script_manager.trigger("variable_label", new_row.doctype, new_row.name); + } + + } + refresh_field("variables"); + } + }); + return frm.call({ + method: "erpnext.buying.doctype.supplier_scorecard_criteria.supplier_scorecard_criteria.get_scoring_criteria", + child: d, + args: { + criteria_name: d.criteria_name + } + }); + } + } +}); + +var loadAllCriteria = function(frm) { + frappe.call({ + method: "erpnext.buying.doctype.supplier_scorecard_criteria.supplier_scorecard_criteria.get_criteria_list", + callback: function(r) { + for (var j = 0; j < frm.doc.criteria.length; j++) + { + if(!frm.doc.criteria[j].hasOwnProperty("criteria_name")) { + frm.get_field("criteria").grid.grid_rows[j].remove(); + } + } + for (var i = 0; i < r.message.length; i++) + { + var new_row = frm.add_child("criteria"); + new_row.criteria_name = r.message[i].name; + frm.script_manager.trigger("criteria_name", new_row.doctype, new_row.name); + } + refresh_field("criteria"); + } + }); +}; +var loadAllStandings = function(frm) { + frappe.call({ + method: "erpnext.buying.doctype.supplier_scorecard_standing.supplier_scorecard_standing.get_standings_list", + callback: function(r) { + for (var j = 0; j < frm.doc.standings.length; j++) + { + if(!frm.doc.standings[j].hasOwnProperty("standing_name")) { + frm.get_field("standings").grid.grid_rows[j].remove(); + } + } + for (var i = 0; i < r.message.length; i++) + { + var new_row = frm.add_child("standings"); + new_row.standing_name = r.message[i].name; + frm.script_manager.trigger("standing_name", new_row.doctype, new_row.name); + } + refresh_field("standings"); + } + }); +}; + + diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.json b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.json new file mode 100644 index 0000000000..d7f24c9082 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.json @@ -0,0 +1,701 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:supplier", + "beta": 1, + "creation": "2017-05-29 01:40:54.786555", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "supplier", + "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": "Supplier", + "length": 0, + "no_copy": 0, + "options": "Supplier", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "supplier_score", + "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": "Supplier Score", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "indicator_color", + "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": "Indicator Color", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "status", + "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": "Status", + "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 + }, + { + "allow_bulk_edit": 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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Per Month", + "fieldname": "period", + "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": "Evaluation Period", + "length": 0, + "no_copy": 0, + "options": "Per Month\nPer Week\nPer Year", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "scoring_setup", + "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": "Scoring Setup", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "{total_score} * max( 0, min ( 1 , (12 - {period_number}) / 12) )", + "description": "Scorecard variables can be used, as well as:\n{total_score} (the total score from that period),\n{period_number} (the number of periods to present day)\n", + "fieldname": "weighting_function", + "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": "Weighting Function", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standings", + "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": "Scoring Standings", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard Scoring Standing", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "criteria_setup", + "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": "Criteria Setup", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "criteria", + "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": "Scoring Criteria", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard Scoring Criteria", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "variables", + "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": "Supplier Variables", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard Scoring Variable", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "eval: doc.status != 'Unknown'", + "columns": 0, + "fieldname": "scorecard_actions", + "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": "Scorecard Actions", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warn_rfqs", + "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": "Warn for new Request for Quotations", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warn_pos", + "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": "Warn for new Purchase Orders", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "prevent_rfqs", + "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": "Prevent RFQs", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "prevent_pos", + "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": "Prevent POs", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_16", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notify_supplier", + "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": "Notify Supplier", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notify_employee", + "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": "Notify Employee", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "employee", + "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": "Employee", + "length": 0, + "no_copy": 0, + "options": "Employee", + "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, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-12 07:33:11.874949", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Scorecard", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "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": 0, + "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 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py new file mode 100644 index 0000000000..e13d22ab57 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import throw, _ +from frappe.model.document import Document +import time +from datetime import timedelta +from frappe.utils import nowdate, get_last_day, getdate, add_days, add_years +from erpnext.buying.doctype.supplier_scorecard_period.supplier_scorecard_period import make_supplier_scorecard + +class SupplierScorecard(Document): + + def validate(self): + self.validate_standings() + self.validate_criteria_weights() + self.calculate_total_score() + self.update_standing() + + def on_update(self): + score = make_all_scorecards(self.name) + if score > 0: + self.save() + + def validate_standings(self): + # Check that there are no overlapping scores and check that there are no missing scores + score = 0 + for c1 in self.standings: + for c2 in self.standings: + if c1 != c2: + if (c1.max_grade > c2.min_grade and c1.min_grade < c2.max_grade): + throw(_('Overlap in scoring between {0} and {1}').format(c1.standing_name,c2.standing_name)) + if c2.min_grade == score: + score = c2.max_grade + if score < 100: + throw(_('Unable to find score starting at {0}. You need to have standing scores covering 0 to 100').format(score)) + + def validate_criteria_weights(self): + + weight = 0 + for c in self.criteria: + weight += c.weight + + if weight != 100: + throw(_('Criteria weights must add up to 100%')) + + def calculate_total_score(self): + scorecards = frappe.db.sql(""" + SELECT + scp.name + FROM + `tabSupplier Scorecard Period` scp + WHERE + scp.scorecard = %(sc)s + ORDER BY + scp.end_date DESC""", + {"sc": self.name}, as_dict=1) + + period = 0 + total_score = 0 + total_max_score = 0 + for scp in scorecards: + my_sc = frappe.get_doc('Supplier Scorecard Period', scp.name) + my_scp_weight = self.weighting_function + my_scp_weight = my_scp_weight.replace('{period_number}', str(period)) + + my_scp_maxweight = my_scp_weight.replace('{total_score}', '100') + my_scp_weight = my_scp_weight.replace('{total_score}', str(my_sc.total_score)) + + max_score = my_sc.calculate_weighted_score(my_scp_maxweight) + score = my_sc.calculate_weighted_score(my_scp_weight) + + total_score += score + total_max_score += max_score + period += 1 + if total_max_score > 0: + self.supplier_score = round(100.0 * (total_score / total_max_score) ,1) + else: + self.supplier_score = 100 + + def update_standing(self): + # Get the setup document + + for standing in self.standings: + if (not standing.min_grade or (standing.min_grade <= self.supplier_score)) and \ + (not standing.max_grade or (standing.max_grade > self.supplier_score)): + self.status = standing.standing_name + self.indicator_color = standing.standing_color + self.notify_supplier = standing.notify_supplier + self.notify_employee = standing.notify_employee + self.employee_link = standing.employee_link + + #Update supplier standing info + for fieldname in ('prevent_pos', 'prevent_rfqs','warn_rfqs','warn_pos'): + self.set(fieldname, standing.get(fieldname)) + frappe.db.set_value("Supplier", self.supplier, fieldname, self.get(fieldname)) + + +@frappe.whitelist() +def get_timeline_data(doctype, name): + # Get a list of all the associated scorecards + scs = frappe.get_doc(doctype, name) + out = {} + timeline_data = {} + scorecards = frappe.db.sql(""" + SELECT + sc.name + FROM + `tabSupplier Scorecard Period` sc + WHERE + sc.scorecard = %(scs)s""", + {"scs": scs.name}, as_dict=1) + + for sc in scorecards: + start_date, end_date, total_score = frappe.db.get_value('Supplier Scorecard Period', sc.name, ['start_date', 'end_date', 'total_score']) + for single_date in daterange(start_date, end_date): + timeline_data[time.mktime(single_date.timetuple())] = total_score + + out['timeline_data'] = timeline_data + return out + +def daterange(start_date, end_date): + for n in range(int ((end_date - start_date).days)+1): + yield start_date + timedelta(n) + +def refresh_scorecards(): + scorecards = frappe.db.sql(""" + SELECT + sc.name + FROM + `tabSupplier Scorecard` sc""", + {}, as_dict=1) + for sc in scorecards: + # Check to see if any new scorecard periods are created + if make_all_scorecards(sc.name) > 0: + # Save the scorecard to update the score and standings + sc.save() + + +@frappe.whitelist() +def make_all_scorecards(docname): + + sc = frappe.get_doc('Supplier Scorecard', docname) + supplier = frappe.get_doc('Supplier',sc.supplier) + + start_date = getdate(supplier.creation) + end_date = get_scorecard_date(sc.period, start_date) + todays = getdate(nowdate()) + + scp_count = 0 + first_start_date = todays + last_end_date = todays + + while (start_date < todays) and (end_date <= todays): + # check to make sure there is no scorecard period already created + scorecards = frappe.db.sql(""" + SELECT + scp.name + FROM + `tabSupplier Scorecard Period` scp + WHERE + scp.scorecard = %(sc)s + AND ( + (scp.start_date > %(end_date)s + AND scp.end_date < %(start_date)s) + OR + (scp.start_date < %(end_date)s + AND scp.end_date > %(start_date)s)) + ORDER BY + scp.end_date DESC""", + {"sc": docname, "start_date": start_date, "end_date": end_date, "supplier": supplier}, as_dict=1) + if len(scorecards) == 0: + period_card = make_supplier_scorecard(docname, None) + period_card.start_date = start_date + period_card.end_date = end_date + period_card.save() + scp_count = scp_count + 1 + if start_date < first_start_date: + first_start_date = start_date + last_end_date = end_date + + start_date = getdate(add_days(end_date,1)) + end_date = get_scorecard_date(sc.period, start_date) + if scp_count > 0: + frappe.msgprint(_("Created {0} scorecards for {1} between: ").format(scp_count, sc.supplier) + str(first_start_date) + " - " + str(last_end_date)) + return scp_count + +def get_scorecard_date(period, start_date): + if period == 'Per Week': + end_date = getdate(add_days(start_date,7)) + elif period == 'Per Month': + end_date = get_last_day(start_date) + elif period == 'Per Year': + end_date = add_days(add_years(start_date,1), -1) + return end_date + +def make_default_records(): + install_variable_docs = [ + {"param_name": "total_accepted_items", "variable_label": "Total Accepted Items", \ + "path": "get_total_accepted_items"}, + {"param_name": "total_accepted_amount", "variable_label": "Total Accepted Amount", \ + "path": "get_total_accepted_amount"}, + {"param_name": "total_rejected_items", "variable_label": "Total Rejected Items", \ + "path": "get_total_rejected_items"}, + {"param_name": "total_rejected_amount", "variable_label": "Total Rejected Amount", \ + "path": "get_total_rejected_amount"}, + {"param_name": "total_received_items", "variable_label": "Total Received Items", \ + "path": "get_total_received_items"}, + {"param_name": "total_received_amount", "variable_label": "Total Received Amount", \ + "path": "get_total_received_amount"}, + {"param_name": "rfq_response_days", "variable_label": "RFQ Response Days", \ + "path": "get_rfq_response_days"}, + {"param_name": "sq_total_items", "variable_label": "SQ Total Items", \ + "path": "get_sq_total_items"}, + {"param_name": "sq_total_number", "variable_label": "SQ Total Number", \ + "path": "get_sq_total_number"}, + {"param_name": "rfq_total_number", "variable_label": "RFQ Total Number", \ + "path": "get_rfq_total_number"}, + {"param_name": "rfq_total_items", "variable_label": "RFQ Total Items", \ + "path": "get_rfq_total_items"}, + {"param_name": "tot_item_days", "variable_label": "Total Item Days", \ + "path": "get_item_workdays"}, + {"param_name": "on_time_shipment_num", "variable_label": "# of On Time Shipments", "path": \ + "get_on_time_shipments"}, + {"param_name": "cost_of_delayed_shipments", "variable_label": "Cost of Delayed Shipments", \ + "path": "get_cost_of_delayed_shipments"}, + {"param_name": "cost_of_on_time_shipments", "variable_label": "Cost of On Time Shipments", \ + "path": "get_cost_of_on_time_shipments"}, + {"param_name": "total_working_days", "variable_label": "Total Working Days", \ + "path": "get_total_workdays"}, + {"param_name": "tot_cost_shipments", "variable_label": "Total Cost of Shipments", \ + "path": "get_total_cost_of_shipments"}, + {"param_name": "tot_days_late", "variable_label": "Total Days Late", \ + "path": "get_total_days_late"}, + {"param_name": "total_shipments", "variable_label": "Total Shipments", \ + "path": "get_total_shipments"} + ] + install_standing_docs = [ + {"min_grade": 0.0, "prevent_rfqs": 1, "notify_supplier": 0, "max_grade": 30.0, "prevent_pos": 1, \ + "standing_color": "Red", "notify_employee": 0, "standing_name": "Very Poor"}, + {"min_grade": 30.0, "prevent_rfqs": 1, "notify_supplier": 0, "max_grade": 50.0, "prevent_pos": 0, \ + "standing_color": "Red", "notify_employee": 0, "standing_name": "Poor"}, + {"min_grade": 50.0, "prevent_rfqs": 0, "notify_supplier": 0, "max_grade": 80.0, "prevent_pos": 0, \ + "standing_color": "Green", "notify_employee": 0, "standing_name": "Average"}, + {"min_grade": 80.0, "prevent_rfqs": 0, "notify_supplier": 0, "max_grade": 100.0, "prevent_pos": 0, \ + "standing_color": "Blue", "notify_employee": 0, "standing_name": "Excellent"}, + ] + + for d in install_variable_docs: + try: + d['doctype'] = "Supplier Scorecard Variable" + frappe.get_doc(d).insert() + except frappe.NameError: + pass + for d in install_standing_docs: + try: + d['doctype'] = "Supplier Scorecard Standing" + frappe.get_doc(d).insert() + except frappe.NameError: + pass diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py new file mode 100644 index 0000000000..ff7f119253 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py @@ -0,0 +1,15 @@ +from frappe import _ + +def get_data(): + return { + 'heatmap': True, + 'heatmap_message': _('This covers all scorecards tied to this Setup'), + 'fieldname': 'supplier', + 'method' : 'erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.get_timeline_data', + 'transactions': [ + { + 'label': _('Scorecards'), + 'items': ['Supplier Scorecard Period'] + } + ] + } \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js new file mode 100644 index 0000000000..c50916e4fa --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js @@ -0,0 +1,17 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +/* global frappe, __ */ + +frappe.listview_settings["Supplier Scorecard"] = { + add_fields: ["indicator_color", "status"], + get_indicator: function(doc) { + + if (doc.indicator_color) { + return [__(doc.status), doc.indicator_color.toLowerCase(), "status,=," + doc.status]; + } else { + return [__("Unknown"), "darkgrey", "status,=,''"]; + } + }, + +}; diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py new file mode 100644 index 0000000000..d64d3f683f --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestSupplierScorecard(unittest.TestCase): + + def test_create_scorecard(self): + delete_test_scorecards() + my_doc = make_supplier_scorecard() + doc = my_doc.insert() + self.assertEqual(doc.name, valid_scorecard[0].get("supplier")) + + def test_criteria_weight(self): + delete_test_scorecards() + my_doc = make_supplier_scorecard() + for d in my_doc.criteria: + d.weight = 0 + self.assertRaises(frappe.ValidationError,my_doc.insert) + + def test_missing_variable(self): + delete_test_scorecards() + my_doc = make_supplier_scorecard() + del my_doc.variables + self.assertRaises(frappe.ValidationError,my_doc.insert) + +def make_supplier_scorecard(): + my_doc = frappe.get_doc(valid_scorecard[0]) + + # Make sure the criteria exist (making them) + for d in valid_scorecard[0].get("criteria"): + if not frappe.db.exists("Supplier Scorecard Criteria", d.get("criteria_name")): + d["doctype"] = "Supplier Scorecard Criteria" + d["name"] = d.get("criteria_name") + my_criteria = frappe.get_doc(d) + my_criteria.insert() + return my_doc + + +def delete_test_scorecards(): + my_doc = make_supplier_scorecard() + if frappe.db.exists("Supplier Scorecard", my_doc.name): + # Delete all the periods, then delete the scorecard + frappe.db.sql("""delete from `tabSupplier Scorecard Period` where scorecard = %(scorecard)s""", {'scorecard': my_doc.name}) + frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'""") + frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'""") + frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'""") + frappe.delete_doc(my_doc.doctype, my_doc.name) + +valid_scorecard = [ + { + "standings":[ + { + "min_grade":0.0,"name":"Very Poor", + "prevent_rfqs":1, + "notify_supplier":0, + "doctype":"Supplier Scorecard Standing", + "max_grade":30.0, + "prevent_pos":1, + "warn_pos":0, + "warn_rfqs":0, + "standing_color":"Red", + "notify_employee":0, + "standing_name":"Very Poor", + "parenttype":"Supplier Scorecard", + "parentfield":"standings" + }, + { + "min_grade":30.0, + "name":"Poor", + "prevent_rfqs":1, + "notify_supplier":0, + "doctype":"Supplier Scorecard Standing", + "max_grade":50.0, + "prevent_pos":0, + "warn_pos":0, + "warn_rfqs":0, + "standing_color":"Red", + "notify_employee":0, + "standing_name":"Poor", + "parenttype":"Supplier Scorecard", + "parentfield":"standings" + }, + { + "min_grade":50.0, + "name":"Average", + "prevent_rfqs":0, + "notify_supplier":0, + "doctype":"Supplier Scorecard Standing", + "max_grade":80.0, + "prevent_pos":0, + "warn_pos":0, + "warn_rfqs":0, + "standing_color":"Green", + "notify_employee":0, + "standing_name":"Average", + "parenttype":"Supplier Scorecard", + "parentfield":"standings" + }, + { + "min_grade":80.0, + "name":"Excellent", + "prevent_rfqs":0, + "notify_supplier":0, + "doctype":"Supplier Scorecard Standing", + "max_grade":100.0, + "prevent_pos":0, + "warn_pos":0, + "warn_rfqs":0, + "standing_color":"Blue", + "notify_employee":0, + "standing_name":"Excellent", + "parenttype":"Supplier Scorecard", + "parentfield":"standings" + } + ], + "prevent_pos":0, + "variables": [ + { + "param_name":"cost_of_on_time_shipments", + "doctype":"Supplier Scorecard Scoring Variable", + "parenttype":"Supplier Scorecard", + "variable_label":"Cost of On Time Shipments", + "path":"get_cost_of_on_time_shipments", + "parentfield":"variables" + }, + { + "param_name":"tot_cost_shipments", + "doctype":"Supplier Scorecard Scoring Variable", + "parenttype":"Supplier Scorecard", + "variable_label":"Total Cost of Shipments", + "path":"get_total_cost_of_shipments", + "parentfield":"variables" + }, + { + "param_name":"tot_days_late", + "doctype":"Supplier Scorecard Scoring Variable", + "parenttype":"Supplier Scorecard", + "variable_label":"Total Days Late", + "path":"get_total_days_late", + "parentfield":"variables" + }, + { + "param_name":"total_working_days", + "doctype":"Supplier Scorecard Scoring Variable", + "parenttype":"Supplier Scorecard", + "variable_label":"Total Working Days", + "path":"get_total_workdays", + "parentfield":"variables" + }, + { + "param_name":"on_time_shipment_num", + "doctype":"Supplier Scorecard Scoring Variable", + "parenttype":"Supplier Scorecard", + "variable_label":"# of On Time Shipments", + "path":"get_on_time_shipments", + "parentfield":"variables" + }, + { + "param_name":"total_shipments", + "doctype":"Supplier Scorecard Scoring Variable", + "parenttype":"Supplier Scorecard", + "variable_label":"Total Shipments", + "path":"get_total_shipments", + "parentfield":"variables" + } + ], + "period":"Per Month", + "doctype":"Supplier Scorecard", + "warn_pos":0, + "warn_rfqs":0, + "notify_supplier":0, + "criteria":[ + { + "weight":100.0, + "doctype":"Supplier Scorecard Scoring Criteria", + "formula":"(({cost_of_on_time_shipments} / {tot_cost_shipments}) if {tot_cost_shipments} > 0 else 1 )* 100 ", + "criteria_name":"Delivery", + "max_score":100.0, + } + ], + "supplier":"_Test Supplier", + "name":"_Test Supplier", + "weighting_function":"{total_score} * max( 0, min ( 1 , (12 - {period_number}) / 12) )", + } +] + diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/__init__.py b/erpnext/buying/doctype/supplier_scorecard_criteria/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js new file mode 100644 index 0000000000..9f8a2dee81 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js @@ -0,0 +1,8 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +/* global frappe */ + +frappe.ui.form.on("Supplier Scorecard Criteria", { + refresh: function() {} +}); diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.json b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.json new file mode 100644 index 0000000000..229c386120 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.json @@ -0,0 +1,184 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:criteria_name", + "beta": 1, + "creation": "2017-05-29 01:32:43.064891", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "criteria_name", + "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": "Criteria Name", + "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, + "unique": 1 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "weight", + "fieldtype": "Percent", + "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": "Criteria Weight", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "100", + "fieldname": "max_score", + "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": "Max Score", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "formula", + "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": "Criteria 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": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-17 10:30:47.458285", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Scorecard Criteria", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "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": 0, + "write": 1 + } + ], + "quick_entry": 1, + "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 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py new file mode 100644 index 0000000000..8514022b78 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +import re +from frappe.model.document import Document + +class InvalidFormulaVariable(frappe.ValidationError): pass + +class SupplierScorecardCriteria(Document): + def validate(self): + self.validate_variables() + self.validate_formula() + + def validate_variables(self): + # make sure all the variables exist + _get_variables(self) + + def validate_formula(self): + # evaluate the formula with 0's to make sure it is valid + test_formula = self.formula.replace("\r", "").replace("\n", "") + + regex = r"\{(.*?)\}" + + mylist = re.finditer(regex, test_formula, re.MULTILINE | re.DOTALL) + for dummy1, match in enumerate(mylist): + for dummy2 in range(0, len(match.groups())): + test_formula = test_formula.replace('{' + match.group(1) + '}', "0") + + test_formula = test_formula.replace('<','<').replace('>','>') + try: + frappe.safe_eval(test_formula, None, {'max':max, 'min': min}) + except Exception: + frappe.throw(_("Error evaluating the criteria formula")) + + + +@frappe.whitelist() +def get_scoring_criteria(criteria_name): + criteria = frappe.get_doc("Supplier Scorecard Criteria", criteria_name) + + return criteria + + +@frappe.whitelist() +def get_criteria_list(): + criteria = frappe.db.sql(""" + SELECT + scs.name + FROM + `tabSupplier Scorecard Criteria` scs""", + {}, as_dict=1) + + return criteria + +@frappe.whitelist() +def get_variables(criteria_name): + criteria = frappe.get_doc("Supplier Scorecard Criteria", criteria_name) + return _get_variables(criteria) + +def _get_variables(criteria): + my_variables = [] + regex = r"\{(.*?)\}" + + mylist = re.finditer(regex, criteria.formula, re.MULTILINE | re.DOTALL) + for dummy1, match in enumerate(mylist): + for dummy2 in range(0, len(match.groups())): + try: + #var = frappe.get_doc("Supplier Scorecard Variable", {'param_name' : d}) + var = frappe.db.sql(""" + SELECT + scv.name + FROM + `tabSupplier Scorecard Variable` scv + WHERE + param_name=%(param)s""", + {'param':match.group(1)},)[0][0] + my_variables.append(var) + except Exception: + # Ignore the ones where the variable can't be found + frappe.throw(_('Unable to find variable: ') + str(match.group(1)), InvalidFormulaVariable) + #pass + + + #frappe.msgprint(str(my_variables)) + return my_variables diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py new file mode 100644 index 0000000000..4eef4b4e03 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestSupplierScorecardCriteria(unittest.TestCase): + def test_variables_exist(self): + delete_test_scorecards() + for d in test_good_criteria: + frappe.get_doc(d).insert() + + self.assertRaises(frappe.ValidationError,frappe.get_doc(test_bad_criteria[0]).insert) + + def test_formula_validate(self): + delete_test_scorecards() + self.assertRaises(frappe.ValidationError,frappe.get_doc(test_bad_criteria[1]).insert) + self.assertRaises(frappe.ValidationError,frappe.get_doc(test_bad_criteria[2]).insert) + +def delete_test_scorecards(): + # Delete all the periods so we can delete all the criteria + frappe.db.sql("""delete from `tabSupplier Scorecard Period`""") + frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'""") + frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'""") + frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'""") + + for d in test_good_criteria: + if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")): + # Delete all the periods, then delete the scorecard + frappe.delete_doc(d.get("doctype"), d.get("name")) + + for d in test_bad_criteria: + if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")): + # Delete all the periods, then delete the scorecard + frappe.delete_doc(d.get("doctype"), d.get("name")) + +test_good_criteria = [ + { + "name":"Delivery", + "weight":40.0, + "doctype":"Supplier Scorecard Criteria", + "formula":"(({cost_of_on_time_shipments} / {tot_cost_shipments}) if {tot_cost_shipments} > 0 else 1 )* 100", + "criteria_name":"Delivery", + "max_score":100.0 + }, +] + +test_bad_criteria = [ + { + "name":"Fake Criteria 1", + "weight":40.0, + "doctype":"Supplier Scorecard Criteria", + "formula":"(({fake_variable} / {tot_cost_shipments}) if {tot_cost_shipments} > 0 else 1 )* 100", # Invalid variable name + "criteria_name":"Fake Criteria 1", + "max_score":100.0 + }, + { + "name":"Fake Criteria 2", + "weight":40.0, + "doctype":"Supplier Scorecard Criteria", + "formula":"(({cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Force 0 divided by 0 + "criteria_name":"Fake Criteria 2", + "max_score":100.0 + }, + { + "name":"Fake Criteria 3", + "weight":40.0, + "doctype":"Supplier Scorecard Criteria", + "formula":"(({cost_of_on_time_shipments} {cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Two variables beside eachother + "criteria_name":"Fake Criteria 3", + "max_score":100.0 + }, +] \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_period/__init__.py b/erpnext/buying/doctype/supplier_scorecard_period/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js new file mode 100644 index 0000000000..c51e8ab2df --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js @@ -0,0 +1,14 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +/* global frappe */ + + +frappe.ui.form.on("Supplier Scorecard Period", { + onload: function(frm) { + frm.get_field("variables").grid.toggle_display("value", true); + frm.get_field("criteria").grid.toggle_display("score", true); + + + } +}); diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.json b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.json new file mode 100644 index 0000000000..0cf651454b --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.json @@ -0,0 +1,397 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "naming_series:", + "beta": 1, + "creation": "2017-05-30 00:38:18.773013", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "supplier", + "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": "Supplier", + "length": 0, + "no_copy": 0, + "options": "Supplier", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 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": "SSC-", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "total_score", + "fieldtype": "Percent", + "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": "Period Score", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "start_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": "Start 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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "end_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": "End 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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "section_break_11", + "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": "Calculations", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "criteria", + "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": "Criteria", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard Scoring Criteria", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "variables", + "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": "Variables", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard Scoring Variable", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "sec_ref", + "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": "Reference", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scorecard", + "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": "Supplier Scorecard Setup", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard", + "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, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-12 07:33:26.130861", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Scorecard Period", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "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": 0, + "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 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py new file mode 100644 index 0000000000..90b65bd35a --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import throw, _ +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +import erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable as variable_functions + +class SupplierScorecardPeriod(Document): + + def validate(self): + self.validate_criteria_weights() + self.calculate_variables() + self.calculate_criteria() + self.calculate_score() + + def validate_criteria_weights(self): + + weight = 0 + for c in self.criteria: + weight += c.weight + + if weight != 100: + throw(_('Criteria weights must add up to 100%')) + + def calculate_variables(self): + for var in self.variables: + + if '.' in var.path: + method_to_call = import_string_path(var.path) + var.value = method_to_call(self) + else: + method_to_call = getattr(variable_functions, var.path) + var.value = method_to_call(self) + + + + def calculate_criteria(self): + #Get the criteria + for crit in self.criteria: + + #me = "" + my_eval_statement = crit.formula.replace("\r", "").replace("\n", "") + #for let in my_eval_statement: + # me += let.encode('hex') + " " + #frappe.msgprint(me) + + for var in self.variables: + if var.value: + if var.param_name in my_eval_statement: + my_eval_statement = my_eval_statement.replace('{' + var.param_name + '}', "{:.2f}".format(var.value)) + else: + if var.param_name in my_eval_statement: + my_eval_statement = my_eval_statement.replace('{' + var.param_name + '}', '0.0') + + #frappe.msgprint(my_eval_statement ) + + my_eval_statement = my_eval_statement.replace('<','<').replace('>','>') + + try: + crit.score = min(crit.max_score, max( 0 ,frappe.safe_eval(my_eval_statement, None, {'max':max, 'min': min}))) + except Exception: + frappe.throw(_("Could not solve criteria score function for {0}. Make sure the formula is valid.".format(crit.criteria_name)),frappe.ValidationError) + crit.score = 0 + + def calculate_score(self): + myscore = 0 + for crit in self.criteria: + myscore += crit.score * crit.weight/100.0 + self.total_score = myscore + + def calculate_weighted_score(self, weighing_function): + my_eval_statement = weighing_function.replace("\r", "").replace("\n", "") + + for var in self.variables: + if var.value: + if var.param_name in my_eval_statement: + my_eval_statement = my_eval_statement.replace('{' + var.param_name + '}', "{:.2f}".format(var.value)) + else: + if var.param_name in my_eval_statement: + my_eval_statement = my_eval_statement.replace('{' + var.param_name + '}', '0.0') + + my_eval_statement = my_eval_statement.replace('<','<').replace('>','>') + + try: + weighed_score = frappe.safe_eval(my_eval_statement, None, {'max':max, 'min': min}) + except Exception: + frappe.throw(_("Could not solve weighted score function. Make sure the formula is valid."),frappe.ValidationError) + weighed_score = 0 + return weighed_score + + + +def import_string_path(path): + components = path.split('.') + mod = __import__(components[0]) + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + + +def post_process(source, target): + pass + + +@frappe.whitelist() +def make_supplier_scorecard(source_name, target_doc=None): + #def update_item(obj, target, source_parent): + # target.qty = flt(obj.qty) - flt(obj.received_qty) + # target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor) + # target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) + # target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \ + # flt(obj.rate) * flt(source_parent.conversion_rate) + + doc = get_mapped_doc("Supplier Scorecard", source_name, { + "Supplier Scorecard": { + "doctype": "Supplier Scorecard Period" + }, + "Supplier Scorecard Scoring Variable": { + "doctype": "Supplier Scorecard Scoring Variable", + "add_if_empty": True + }, + "Supplier Scorecard Scoring Constraint": { + "doctype": "Supplier Scorecard Scoring Constraint", + "add_if_empty": True + } + }, target_doc, post_process) + + return doc + diff --git a/erpnext/buying/doctype/supplier_scorecard_period/test_supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/test_supplier_scorecard_period.py new file mode 100644 index 0000000000..8baa3185ba --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_period/test_supplier_scorecard_period.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestSupplierScorecardPeriod(unittest.TestCase): + pass diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_criteria/__init__.py b/erpnext/buying/doctype/supplier_scorecard_scoring_criteria/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_criteria/supplier_scorecard_scoring_criteria.json b/erpnext/buying/doctype/supplier_scorecard_scoring_criteria/supplier_scorecard_scoring_criteria.json new file mode 100644 index 0000000000..567738a6d0 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_scoring_criteria/supplier_scorecard_scoring_criteria.json @@ -0,0 +1,280 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 1, + "creation": "2017-05-29 01:32:17.988454", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 3, + "fieldname": "criteria_name", + "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": "Criteria Name", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard Criteria", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_2", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 2, + "fieldname": "weight", + "fieldtype": "Percent", + "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": "Criteria Weight", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "100", + "fieldname": "max_score", + "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": "Max Score", + "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 + }, + { + "allow_bulk_edit": 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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "formula", + "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": "Criteria 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": 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": "score", + "fieldtype": "Percent", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Score", + "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 + } + ], + "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": "2017-07-12 07:33:41.532361", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Scorecard Scoring Criteria", + "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": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_criteria/supplier_scorecard_scoring_criteria.py b/erpnext/buying/doctype/supplier_scorecard_scoring_criteria/supplier_scorecard_scoring_criteria.py new file mode 100644 index 0000000000..b64abed8a6 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_scoring_criteria/supplier_scorecard_scoring_criteria.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class SupplierScorecardScoringCriteria(Document): + pass diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_standing/__init__.py b/erpnext/buying/doctype/supplier_scorecard_scoring_standing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_standing/supplier_scorecard_scoring_standing.json b/erpnext/buying/doctype/supplier_scorecard_scoring_standing/supplier_scorecard_scoring_standing.json new file mode 100644 index 0000000000..1fc04bb120 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_scoring_standing/supplier_scorecard_scoring_standing.json @@ -0,0 +1,491 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 1, + "creation": "2017-05-29 01:36:22.697234", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 3, + "fieldname": "standing_name", + "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": "Standing Name", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard Standing", + "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 + }, + { + "allow_bulk_edit": 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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standing_color", + "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": "Color", + "length": 0, + "no_copy": 0, + "options": "Blue\nPurple\nGreen\nYellow\nOrange\nRed", + "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 + }, + { + "allow_bulk_edit": 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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 2, + "fieldname": "min_grade", + "fieldtype": "Percent", + "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": "Min Grade", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 2, + "fieldname": "max_grade", + "fieldtype": "Percent", + "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": "Max Grade", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "actions", + "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": "Actions", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warn_rfqs", + "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": "Warn RFQs", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warn_pos", + "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": "Warn Purchase Orders", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "prevent_rfqs", + "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": "Prevent RFQs", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "prevent_pos", + "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": "Prevent Purchase Orders", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_10", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notify_supplier", + "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": "Notify Supplier", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notify_employee", + "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": "Notify Employee", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "employee_link", + "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": "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, + "unique": 0 + } + ], + "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": "2017-07-12 07:33:20.615684", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Scorecard Scoring Standing", + "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": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_standing/supplier_scorecard_scoring_standing.py b/erpnext/buying/doctype/supplier_scorecard_scoring_standing/supplier_scorecard_scoring_standing.py new file mode 100644 index 0000000000..e8ad79f33d --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_scoring_standing/supplier_scorecard_scoring_standing.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class SupplierScorecardScoringStanding(Document): + pass diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_variable/__init__.py b/erpnext/buying/doctype/supplier_scorecard_scoring_variable/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.json b/erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.json new file mode 100644 index 0000000000..f0e043e47a --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.json @@ -0,0 +1,222 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 1, + "creation": "2017-05-29 01:30:06.105240", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 3, + "fieldname": "variable_label", + "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": "Variable Name", + "length": 0, + "no_copy": 0, + "options": "Supplier Scorecard Variable", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 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, + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_custom", + "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": "Custom?", + "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": 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": "param_name", + "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": "Parameter Name", + "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": 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": "path", + "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": "Path", + "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": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 2, + "fieldname": "value", + "fieldtype": "Float", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Value", + "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 + } + ], + "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": "2017-07-12 07:33:36.671502", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Scorecard Scoring Variable", + "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": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.py b/erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.py new file mode 100644 index 0000000000..58a8a99a09 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class SupplierScorecardScoringVariable(Document): + pass diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/__init__.py b/erpnext/buying/doctype/supplier_scorecard_standing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js new file mode 100644 index 0000000000..dccfcc34bb --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js @@ -0,0 +1,10 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +/* global frappe */ + +frappe.ui.form.on("Supplier Scorecard Standing", { + refresh: function() { + + } +}); diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.json b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.json new file mode 100644 index 0000000000..b61b4edd72 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.json @@ -0,0 +1,424 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:standing_name", + "beta": 1, + "creation": "2017-05-29 01:36:47.893639", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standing_name", + "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": "Standing Name", + "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": 1 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standing_color", + "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": "Color", + "length": 0, + "no_copy": 0, + "options": "Blue\nPurple\nGreen\nYellow\nOrange\nRed", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "min_grade", + "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": "Min Grade", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "max_grade", + "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": "Max Grade", + "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 + }, + { + "allow_bulk_edit": 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, + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warn_rfqs", + "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": "Warn RFQs", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warn_pos", + "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": "Warn Purchase Orders", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "prevent_rfqs", + "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": "Prevent RFQs", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "prevent_pos", + "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": "Prevent Purchase Orders", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notify_supplier", + "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": "Notify Supplier", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notify_employee", + "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": "Notify Other", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "employee_link", + "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": "Other", + "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, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-12 07:33:16.560273", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Scorecard Standing", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "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": 0, + "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 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py new file mode 100644 index 0000000000..1ba5d06c53 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class SupplierScorecardStanding(Document): + pass + + +@frappe.whitelist() +def get_scoring_standing(standing_name): + standing = frappe.get_doc("Supplier Scorecard Standing", standing_name) + + return standing + + +@frappe.whitelist() +def get_standings_list(): + standings = frappe.db.sql(""" + SELECT + scs.name + FROM + `tabSupplier Scorecard Standing` scs""", + {}, as_dict=1) + + return standings \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/test_supplier_scorecard_standing.py b/erpnext/buying/doctype/supplier_scorecard_standing/test_supplier_scorecard_standing.py new file mode 100644 index 0000000000..4d96651313 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_standing/test_supplier_scorecard_standing.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestSupplierScorecardStanding(unittest.TestCase): + pass diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/__init__.py b/erpnext/buying/doctype/supplier_scorecard_variable/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js new file mode 100644 index 0000000000..2d74fdd190 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js @@ -0,0 +1,10 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +/* global frappe */ + +frappe.ui.form.on("Supplier Scorecard Variable", { + refresh: function() { + + } +}); diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.json b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.json new file mode 100644 index 0000000000..d24484025c --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.json @@ -0,0 +1,242 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:variable_label", + "beta": 1, + "creation": "2017-05-29 01:30:34.688389", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "variable_label", + "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": "Variable Name", + "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, + "unique": 1 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_custom", + "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": "Custom?", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "param_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": "Parameter Name", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "path", + "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": "Path", + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 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, + "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 + }, + { + "allow_bulk_edit": 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, + "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 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-12 07:33:31.395262", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Scorecard Variable", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "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": 0, + "write": 1 + } + ], + "quick_entry": 1, + "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 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py new file mode 100644 index 0000000000..17c911a000 --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py @@ -0,0 +1,503 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import sys +from frappe import _ +from frappe.model.document import Document +from frappe.utils import getdate + +class VariablePathNotFound(frappe.ValidationError): pass + +class SupplierScorecardVariable(Document): + def validate(self): + self.validate_path_exists() + + def validate_path_exists(self): + if '.' in self.path: + try: + from erpnext.buying.doctype.supplier_scorecard_period.supplier_scorecard_period import import_string_path + import_string_path(self.path) + except AttributeError: + frappe.throw(_("Could not find path for " + self.path), VariablePathNotFound) + + else: + if not hasattr(sys.modules[__name__], self.path): + frappe.throw(_("Could not find path for " + self.path), VariablePathNotFound) + + +@frappe.whitelist() +def get_scoring_variable(variable_label): + variable = frappe.get_doc("Supplier Scorecard Variable", variable_label) + + return variable + +def get_total_workdays(scorecard): + """ Gets the number of days in this period""" + delta = getdate(scorecard.end_date) - getdate(scorecard.start_date) + return delta.days + +def get_item_workdays(scorecard): + """ Gets the number of days in this period""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + total_item_days = frappe.db.sql(""" + SELECT + SUM(DATEDIFF( %(end_date)s, po_item.schedule_date) * (po_item.qty)) + FROM + `tabPurchase Order Item` po_item, + `tabPurchase Order` po + WHERE + po.supplier = %(supplier)s + AND po_item.received_qty < po_item.qty + AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s + AND po_item.parent = po.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not total_item_days: + total_item_days = 0 + return total_item_days + + + +def get_total_cost_of_shipments(scorecard): + """ Gets the total cost of all shipments in the period (based on Purchase Orders)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + SUM(po_item.base_amount) + FROM + `tabPurchase Order Item` po_item, + `tabPurchase Order` po + WHERE + po.supplier = %(supplier)s + AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s + AND po_item.docstatus = 1 + AND po_item.parent = po.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if data: + return data + else: + return 0 + +def get_cost_of_delayed_shipments(scorecard): + """ Gets the total cost of all delayed shipments in the period (based on Purchase Receipts - POs)""" + return get_total_cost_of_shipments(scorecard) - get_cost_of_on_time_shipments(scorecard) + +def get_cost_of_on_time_shipments(scorecard): + """ Gets the total cost of all on_time shipments in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + + total_delivered_on_time_costs = frappe.db.sql(""" + SELECT + SUM(pr_item.base_amount) + FROM + `tabPurchase Order Item` po_item, + `tabPurchase Receipt Item` pr_item, + `tabPurchase Order` po, + `tabPurchase Receipt` pr + WHERE + po.supplier = %(supplier)s + AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s + AND po_item.schedule_date >= pr.posting_date + AND pr_item.docstatus = 1 + AND pr_item.purchase_order_item = po_item.name + AND po_item.parent = po.name + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if total_delivered_on_time_costs: + return total_delivered_on_time_costs + else: + return 0 + + +def get_total_days_late(scorecard): + """ Gets the number of item days late in the period (based on Purchase Receipts vs POs)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + total_delivered_late_days = frappe.db.sql(""" + SELECT + SUM(DATEDIFF(pr.posting_date,po_item.schedule_date)* pr_item.qty) + FROM + `tabPurchase Order Item` po_item, + `tabPurchase Receipt Item` pr_item, + `tabPurchase Order` po, + `tabPurchase Receipt` pr + WHERE + po.supplier = %(supplier)s + AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s + AND po_item.schedule_date < pr.posting_date + AND pr_item.docstatus = 1 + AND pr_item.purchase_order_item = po_item.name + AND po_item.parent = po.name + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + if not total_delivered_late_days: + total_delivered_late_days = 0 + + total_missed_late_days = frappe.db.sql(""" + SELECT + SUM(DATEDIFF( %(end_date)s, po_item.schedule_date) * (po_item.qty - po_item.received_qty)) + FROM + `tabPurchase Order Item` po_item, + `tabPurchase Order` po + WHERE + po.supplier = %(supplier)s + AND po_item.received_qty < po_item.qty + AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s + AND po_item.parent = po.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not total_missed_late_days: + total_missed_late_days = 0 + return total_missed_late_days + total_delivered_late_days + +def get_on_time_shipments(scorecard): + """ Gets the number of late shipments (counting each item) in the period (based on Purchase Receipts vs POs)""" + + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + total_items_delivered_on_time = frappe.db.sql(""" + SELECT + COUNT(pr_item.qty) + FROM + `tabPurchase Order Item` po_item, + `tabPurchase Receipt Item` pr_item, + `tabPurchase Order` po, + `tabPurchase Receipt` pr + WHERE + po.supplier = %(supplier)s + AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s + AND po_item.schedule_date <= pr.posting_date + AND po_item.qty = pr_item.qty + AND pr_item.docstatus = 1 + AND pr_item.purchase_order_item = po_item.name + AND po_item.parent = po.name + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not total_items_delivered_on_time: + total_items_delivered_on_time = 0 + return total_items_delivered_on_time + +def get_late_shipments(scorecard): + """ Gets the number of late shipments (counting each item) in the period (based on Purchase Receipts vs POs)""" + return get_total_shipments(scorecard) - get_on_time_shipments(scorecard) + +def get_total_received(scorecard): + """ Gets the total number of received shipments in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + COUNT(pr_item.base_amount) + FROM + `tabPurchase Receipt Item` pr_item, + `tabPurchase Receipt` pr + WHERE + pr.supplier = %(supplier)s + AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s + AND pr_item.docstatus = 1 + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_total_received_amount(scorecard): + """ Gets the total amount (in company currency) received in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + SUM(pr_item.received_qty * pr_item.base_rate) + FROM + `tabPurchase Receipt Item` pr_item, + `tabPurchase Receipt` pr + WHERE + pr.supplier = %(supplier)s + AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s + AND pr_item.docstatus = 1 + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_total_received_items(scorecard): + """ Gets the total number of received shipments in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + SUM(pr_item.received_qty) + FROM + `tabPurchase Receipt Item` pr_item, + `tabPurchase Receipt` pr + WHERE + pr.supplier = %(supplier)s + AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s + AND pr_item.docstatus = 1 + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_total_rejected_amount(scorecard): + """ Gets the total amount (in company currency) rejected in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + SUM(pr_item.rejected_qty * pr_item.base_rate) + FROM + `tabPurchase Receipt Item` pr_item, + `tabPurchase Receipt` pr + WHERE + pr.supplier = %(supplier)s + AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s + AND pr_item.docstatus = 1 + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_total_rejected_items(scorecard): + """ Gets the total number of rejected items in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + SUM(pr_item.rejected_qty) + FROM + `tabPurchase Receipt Item` pr_item, + `tabPurchase Receipt` pr + WHERE + pr.supplier = %(supplier)s + AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s + AND pr_item.docstatus = 1 + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_total_accepted_amount(scorecard): + """ Gets the total amount (in company currency) accepted in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + SUM(pr_item.qty * pr_item.base_rate) + FROM + `tabPurchase Receipt Item` pr_item, + `tabPurchase Receipt` pr + WHERE + pr.supplier = %(supplier)s + AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s + AND pr_item.docstatus = 1 + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_total_accepted_items(scorecard): + """ Gets the total number of rejected items in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + SUM(pr_item.qty) + FROM + `tabPurchase Receipt Item` pr_item, + `tabPurchase Receipt` pr + WHERE + pr.supplier = %(supplier)s + AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s + AND pr_item.docstatus = 1 + AND pr_item.parent = pr.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_total_shipments(scorecard): + """ Gets the total number of ordered shipments to arrive in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + COUNT(po_item.base_amount) + FROM + `tabPurchase Order Item` po_item, + `tabPurchase Order` po + WHERE + po.supplier = %(supplier)s + AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s + AND po_item.docstatus = 1 + AND po_item.parent = po.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_rfq_total_number(scorecard): + """ Gets the total number of RFQs sent to supplier""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + COUNT(rfq.name) as total_rfqs + FROM + `tabRequest for Quotation Item` rfq_item, + `tabRequest for Quotation Supplier` rfq_sup, + `tabRequest for Quotation` rfq + WHERE + rfq_sup.supplier = %(supplier)s + AND rfq.transaction_date BETWEEN %(start_date)s AND %(end_date)s + AND rfq_item.docstatus = 1 + AND rfq_item.parent = rfq.name + AND rfq_sup.parent = rfq.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + + if not data: + data = 0 + return data + +def get_rfq_total_items(scorecard): + """ Gets the total number of RFQ items sent to supplier""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + COUNT(rfq_item.name) as total_rfqs + FROM + `tabRequest for Quotation Item` rfq_item, + `tabRequest for Quotation Supplier` rfq_sup, + `tabRequest for Quotation` rfq + WHERE + rfq_sup.supplier = %(supplier)s + AND rfq.transaction_date BETWEEN %(start_date)s AND %(end_date)s + AND rfq_item.docstatus = 1 + AND rfq_item.parent = rfq.name + AND rfq_sup.parent = rfq.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + if not data: + data = 0 + return data + + +def get_sq_total_number(scorecard): + """ Gets the total number of RFQ items sent to supplier""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + COUNT(sq.name) as total_sqs + FROM + `tabRequest for Quotation Item` rfq_item, + `tabSupplier Quotation Item` sq_item, + `tabRequest for Quotation Supplier` rfq_sup, + `tabRequest for Quotation` rfq, + `tabSupplier Quotation` sq + WHERE + rfq_sup.supplier = %(supplier)s + AND rfq.transaction_date BETWEEN %(start_date)s AND %(end_date)s + AND sq_item.request_for_quotation_item = rfq_item.name + AND sq_item.docstatus = 1 + AND rfq_item.docstatus = 1 + AND sq.supplier = %(supplier)s + AND sq_item.parent = sq.name + AND rfq_item.parent = rfq.name + AND rfq_sup.parent = rfq.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + if not data: + data = 0 + return data + +def get_sq_total_items(scorecard): + """ Gets the total number of RFQ items sent to supplier""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + + # Look up all PO Items with delivery dates between our dates + data = frappe.db.sql(""" + SELECT + COUNT(sq_item.name) as total_sqs + FROM + `tabRequest for Quotation Item` rfq_item, + `tabSupplier Quotation Item` sq_item, + `tabSupplier Quotation` sq, + `tabRequest for Quotation Supplier` rfq_sup, + `tabRequest for Quotation` rfq + WHERE + rfq_sup.supplier = %(supplier)s + AND rfq.transaction_date BETWEEN %(start_date)s AND %(end_date)s + AND sq_item.request_for_quotation_item = rfq_item.name + AND sq_item.docstatus = 1 + AND sq.supplier = %(supplier)s + AND sq_item.parent = sq.name + AND rfq_item.docstatus = 1 + AND rfq_item.parent = rfq.name + AND rfq_sup.parent = rfq.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + if not data: + data = 0 + return data + +def get_rfq_response_days(scorecard): + """ Gets the total number of days it has taken a supplier to respond to rfqs in the period""" + supplier = frappe.get_doc('Supplier', scorecard.supplier) + total_sq_days = frappe.db.sql(""" + SELECT + SUM(DATEDIFF(sq.transaction_date, rfq.transaction_date)) + FROM + `tabRequest for Quotation Item` rfq_item, + `tabSupplier Quotation Item` sq_item, + `tabSupplier Quotation` sq, + `tabRequest for Quotation Supplier` rfq_sup, + `tabRequest for Quotation` rfq + WHERE + rfq_sup.supplier = %(supplier)s + AND rfq.transaction_date BETWEEN %(start_date)s AND %(end_date)s + AND sq_item.request_for_quotation_item = rfq_item.name + AND sq_item.docstatus = 1 + AND sq.supplier = %(supplier)s + AND sq_item.parent = sq.name + AND rfq_item.docstatus = 1 + AND rfq_item.parent = rfq.name + AND rfq_sup.parent = rfq.name""", + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + if not total_sq_days: + total_sq_days = 0 + + + return total_sq_days \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py new file mode 100644 index 0000000000..45a2c6250a --- /dev/null +++ b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import VariablePathNotFound + + +class TestSupplierScorecardVariable(unittest.TestCase): + def test_variable_exist(self): + for d in test_existing_variables: + my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name")) + self.assertEquals(my_doc.param_name, d.get('param_name')) + self.assertEquals(my_doc.variable_label, d.get('variable_label')) + self.assertEquals(my_doc.path, d.get('path')) + + def test_path_exists(self): + for d in test_good_variables: + if frappe.db.exists(d): + frappe.delete_doc(d.get("doctype"), d.get("name")) + frappe.get_doc(d).insert() + + for d in test_bad_variables: + self.assertRaises(VariablePathNotFound,frappe.get_doc(d).insert) + +test_existing_variables = [ + { + "param_name":"total_accepted_items", + "name":"Total Accepted Items", + "doctype":"Supplier Scorecard Variable", + "variable_label":"Total Accepted Items", + "path":"get_total_accepted_items" + }, +] + +test_good_variables = [ + { + "param_name":"good_variable1", + "name":"Good Variable 1", + "doctype":"Supplier Scorecard Variable", + "variable_label":"Good Variable 1", + "path":"get_total_accepted_items" + }, +] + +test_bad_variables = [ + { + "param_name":"fake_variable1", + "name":"Fake Variable 1", + "doctype":"Supplier Scorecard Variable", + "variable_label":"Fake Variable 1", + "path":"get_fake_variable1" + }, +] \ No newline at end of file diff --git a/erpnext/config/buying.py b/erpnext/config/buying.py index 990ca7a8ba..ba29125ca0 100644 --- a/erpnext/config/buying.py +++ b/erpnext/config/buying.py @@ -141,6 +141,32 @@ def get_data(): }, ] }, + { + "label": _("Supplier Scorecard"), + "items": [ + { + "type": "doctype", + "name": "Supplier Scorecard", + "description": _("All Supplier scorecards."), + }, + { + "type": "doctype", + "name": "Supplier Scorecard Variable", + "description": _("Templates of supplier scorecard variables.") + }, + { + "type": "doctype", + "name": "Supplier Scorecard Criteria", + "description": _("Templates of supplier scorecard criteria."), + }, + { + "type": "doctype", + "name": "Supplier Scorecard Standing", + "description": _("Templates of supplier standings."), + }, + + ] + }, { "label": _("Other Reports"), "icon": "fa fa-list", diff --git a/erpnext/docs/assets/img/buying/supplier-scorecard-criteria.png b/erpnext/docs/assets/img/buying/supplier-scorecard-criteria.png new file mode 100644 index 0000000000000000000000000000000000000000..0bc73c81eee61deee61fbcdfd0876edafb33086b GIT binary patch literal 22343 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�e^uWnRk@LEakIGV~t1Q}Ol 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

@fZZ!DfY(1H@>(bdX@}dK!Zk4X=Je792)A8KO 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 Th&U2x0*ABD)7PVZH#+S%d9H1> literal 0 HcmV?d00001 diff --git a/erpnext/docs/assets/img/buying/supplier-scorecard.png b/erpnext/docs/assets/img/buying/supplier-scorecard.png new file mode 100644 index 0000000000000000000000000000000000000000..1f8de1736de42bc0eea96f4d644071acdbab363a GIT binary patch literal 54354 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 b=S@@%W%xl_t;!5=F_DYs?S8HVxc`3w7pXjW literal 0 HcmV?d00001 diff --git a/erpnext/docs/user/manual/en/buying/index.txt b/erpnext/docs/user/manual/en/buying/index.txt index 25c8797e18..4bd75f1047 100644 --- a/erpnext/docs/user/manual/en/buying/index.txt +++ b/erpnext/docs/user/manual/en/buying/index.txt @@ -5,3 +5,4 @@ purchase-order setup articles purchase-taxes +supplier-scorecard \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/buying/supplier-scorecard.md b/erpnext/docs/user/manual/en/buying/supplier-scorecard.md new file mode 100644 index 0000000000..cecdf9cd21 --- /dev/null +++ b/erpnext/docs/user/manual/en/buying/supplier-scorecard.md @@ -0,0 +1,76 @@ +A Supplier Scorecard is an evaluation tool used to assess the performance of +suppliers. Supplier scorecards can be used to keep track of item quality, +delivery and responsiveness of suppliers across long periods of time. This data +is typically used to help in purchasing decisions. + +A Supplier Scorecard is manually created for each supplier. + +In ERPNext, you can create a supplier scorecard by going to: + +> Buying > Documents > Supplier Scorecard > New Supplier Scorecard + +### Create Supplier Scorecard +A supplier scorecard is created for each supplier individually. Only one supplier scorecard can be created for each +supplier. +Purchase Order + +#### Final Score and Standings +The supplier scorecard consists of a set evaluation periods, during which the performance of a supplier is +evaluated. This period can be daily, monthly or yearly. The current score is calculated from the score of each evaluation +period based on the weighting function. The default formula is linearly weight over the previous 12 scoring periods. +Purchase Order +This formula is customizable. + +The supplier standing is used to quickly sort suppliers based on their performance. These are customizable for each supplier. +The scorecard standing of a supplier can also be used to restrict suppliers from being included in Request for Quotations or +being issued Purchase Orders. +Purchase Order + +#### Evaluation Criteria and Variables +A supplier can be evaluated on several individual evaluation criteria, including (but not limited to) quotation response time, +delivered item quality, and delivery timeliness. These criteria are weighed to determine the final period score. +Purchase Order +The method for calculating each criteria is determined through the criteria formula field, which can use a number of pre-established variables. +The value of each of these variables is calculated over the scoring period for each supplier. Examples of such variables include: + - The total number of items received from the supplier + - The total number of accepted items from the supplier + - The total number of rejected items from the supplier + - The total number of deliveries from the supplier + - The total amount (in dollars) received from a supplier +Additional variables can be added through server-side customizations. + +The criteria formula should be customized to evaluate the suppliers in each criteria in a way that best fits the Company requirements. + +##### Evaluation Formulas +The evaluation formula uses the pre-established or custom variables to evaluate an aspect of supplier performance over the scoring period. Formulas can use the following mathematical functions: + +* addition: + +* subtraction: - +* multiplication: * +* division: / +* min: min(x,y) +* max: max(x,y) +* if/else: (x) if (formula) else (y) +* less than: < +* greated than: > +* variables: {variable_name} + +It is crucial that the formula be solvable for all variable values. This is most often an issue if the value resolves to 0. For example: +``` +{total_accepted_items} / {total_received_items} +``` + +This example would resolve to 0 / 0 in periods where there are no received items, and therefore should have a check to protect in this case: +``` +({total_accepted_items} / {total_received_items}) if {total_received_items} > 0 else 1. +``` + +### Evaluating the Supplier +An evaluation is generated for each Supplier Scorecard Period by clicking the "Generate Missing Scorecard Periods" button. The supplier +current score can be seen, as well as a visual graphic showing the performance of the supplier over time. Any actions against the supplier +are also noted here, including warnings when create RFQs and POs or locking out those features for this supplier altogether. + + + + +{next} diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 5f0b19ebd7..860aac2ee7 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -191,6 +191,7 @@ scheduler_events = { "erpnext.accounts.doctype.asset.depreciation.post_depreciation_entries", "erpnext.hr.doctype.daily_work_summary_settings.daily_work_summary_settings.send_summary", "erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status", + "erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards", "erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history" ] } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 834ed2fdf4..00f395fe8c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -422,6 +422,7 @@ erpnext.patches.v8_1.add_indexes_in_transaction_doctypes erpnext.patches.v8_3.set_restrict_to_domain_for_module_def erpnext.patches.v8_1.update_expense_claim_status erpnext.patches.v8_3.update_company_total_sales +erpnext.patches.v8_4.make_scorecard_records erpnext.patches.v8_1.set_delivery_date_in_so_item erpnext.patches.v8_5.fix_tax_breakup_for_non_invoice_docs -erpnext.patches.v8_5.remove_quotations_route_in_sidebar \ No newline at end of file +erpnext.patches.v8_5.remove_quotations_route_in_sidebar diff --git a/erpnext/patches/v8_4/__init__.py b/erpnext/patches/v8_4/__init__.py new file mode 100644 index 0000000000..baffc48825 --- /dev/null +++ b/erpnext/patches/v8_4/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/erpnext/patches/v8_4/make_scorecard_records.py b/erpnext/patches/v8_4/make_scorecard_records.py new file mode 100644 index 0000000000..37789d711a --- /dev/null +++ b/erpnext/patches/v8_4/make_scorecard_records.py @@ -0,0 +1,9 @@ +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import make_default_records +def execute(): + + make_default_records() \ No newline at end of file diff --git a/erpnext/setup/setup_wizard/install_fixtures.py b/erpnext/setup/setup_wizard/install_fixtures.py index 43baf2f4fb..1301d33856 100644 --- a/erpnext/setup/setup_wizard/install_fixtures.py +++ b/erpnext/setup/setup_wizard/install_fixtures.py @@ -213,6 +213,10 @@ def install(country=None): records += [{'doctype': 'Lead Source', 'source_name': _(d)} for d in default_lead_sources] + # Records for the Supplier Scorecard + from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import make_default_records + make_default_records() + from frappe.modules import scrub for r in records: doc = frappe.new_doc(r.get("doctype")) From fcaf313c0f04d7d457e1d0c5cef62c253d6bf76d Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Tue, 25 Jul 2017 10:49:35 +0530 Subject: [PATCH 07/31] [minor] make-demo fixes for manufacturing domain (#10029) --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index ce89589886..7130a3e94d 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -52,8 +52,8 @@ class BOM(WebsiteGenerator): validate_uom_is_integer(self, "stock_uom", "stock_qty", "BOM Item") self.update_stock_qty() - self.validate_materials() self.set_bom_material_details() + self.validate_materials() self.validate_operations() self.calculate_cost() From edb2749dfd23fe685ae0a64b65a5e04f11cb74bf Mon Sep 17 00:00:00 2001 From: Vishal Dhayagude Date: Tue, 25 Jul 2017 10:53:12 +0530 Subject: [PATCH 08/31] Timesheet web (#10037) * [new]Timesheet added * [new] Customer wise timesheet on webportal added --- erpnext/hooks.py | 11 +++++---- .../projects/doctype/timesheet/timesheet.py | 23 +++++++++++++++++++ .../includes/timesheet/timesheet_row.html | 13 +++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 erpnext/templates/includes/timesheet/timesheet_row.html diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 860aac2ee7..37fd8692e5 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -109,7 +109,8 @@ website_route_rules = [ }, {"from_route": "/jobs", "to_route": "Job Opening"}, {"from_route": "/admissions", "to_route": "Student Admission"}, - {"from_route": "/boms", "to_route": "BOM"} + {"from_route": "/boms", "to_route": "BOM"}, + {"from_route": "/timesheets", "to_route": "Timesheet"}, ] standard_portal_menu_items = [ @@ -122,13 +123,14 @@ standard_portal_menu_items = [ {"title": _("Shipments"), "route": "/shipments", "reference_doctype": "Delivery Note", "role":"Customer"}, {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role":"Customer"}, {"title": _("Addresses"), "route": "/addresses", "reference_doctype": "Address"}, - {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role":"Student"} + {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role":"Student"}, + {"title": _("Timesheets"), "route": "/timesheets", "reference_doctype": "Timesheet", "role":"Customer"} ] default_roles = [ {'role': 'Customer', 'doctype':'Contact', 'email_field': 'email_id'}, {'role': 'Supplier', 'doctype':'Contact', 'email_field': 'email_id'}, - {'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'} + {'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'}, ] has_website_permission = { @@ -137,7 +139,8 @@ has_website_permission = { "Sales Invoice": "erpnext.controllers.website_list_for_contact.has_website_permission", "Supplier Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission", "Delivery Note": "erpnext.controllers.website_list_for_contact.has_website_permission", - "Issue": "erpnext.support.doctype.issue.issue.has_website_permission" + "Issue": "erpnext.support.doctype.issue.issue.has_website_permission", + "Timesheet": "erpnext.controllers.website_list_for_contact.has_website_permission" } dump_report_map = "erpnext.startup.report_data_map.data_map" diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 6416176190..95fd420ba4 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -381,3 +381,26 @@ def get_events(start, end, filters=None): "end": end }, as_dict=True, update={"allDay": 0}) +def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): + user = frappe.session.user + # find customer name from contact. + customer = frappe.db.sql('''SELECT dl.link_name FROM `tabContact` AS c inner join \ + `tabDynamic Link` AS dl ON c.first_name=dl.link_name WHERE c.email_id=%s''',user) + # find list of Sales Invoice for made for customer. + sales_invoice = frappe.db.sql('''SELECT name FROM `tabSales Invoice` WHERE customer = %s''',customer) + if customer: + # Return timesheet related data to web portal. + return frappe. db.sql('''SELECT ts.name, tsd.activity_type, ts.status, ts.total_billable_hours, \ + tsd.sales_invoice, tsd.project FROM `tabTimesheet` AS ts inner join `tabTimesheet Detail` \ + AS tsd ON tsd.parent = ts.name where tsd.sales_invoice IN %s order by\ + end_date asc limit {0} , {1}'''.format(limit_start, limit_page_length), [sales_invoice], as_dict = True) + +def get_list_context(context=None): + return { + "show_sidebar": True, + "show_search": True, + 'no_breadcrumbs': True, + "title": _("Timesheets"), + "get_list": get_timesheets_list, + "row_template": "templates/includes/timesheet/timesheet_row.html" + } diff --git a/erpnext/templates/includes/timesheet/timesheet_row.html b/erpnext/templates/includes/timesheet/timesheet_row.html new file mode 100644 index 0000000000..e9cfcda812 --- /dev/null +++ b/erpnext/templates/includes/timesheet/timesheet_row.html @@ -0,0 +1,13 @@ +

From d023d9a0bd9d0837b0fff6148f0be3b10603e293 Mon Sep 17 00:00:00 2001 From: bcornwellmott Date: Mon, 24 Jul 2017 22:33:12 -0700 Subject: [PATCH 09/31] Add RFQ email sent check (#10068) * Add RFQ email sent check * remove blankspace * Removed debugger --- .../request_for_quotation.js | 3 ++ .../request_for_quotation.py | 4 ++ .../request_for_quotation_supplier.json | 44 ++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index ce759aaae8..1c7b5a8e86 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -44,6 +44,9 @@ frappe.ui.form.on("Request for Quotation",{ freeze: true, args: { rfq_name: frm.doc.name + }, + callback: function(r){ + frm.reload_doc(); } }); }); diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 666a1c6e8a..94dc51bdc7 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -52,6 +52,8 @@ class RequestforQuotation(BuyingController): def on_submit(self): frappe.db.set(self, 'status', 'Submitted') + for supplier in self.suppliers: + supplier.email_sent = 0 def on_cancel(self): frappe.db.set(self, 'status', 'Cancelled') @@ -66,6 +68,8 @@ class RequestforQuotation(BuyingController): self.update_supplier_part_no(rfq_supplier) self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link()) + rfq_supplier.email_sent = 1 + rfq_supplier.save() def get_link(self): # RFQ link for supplier portal diff --git a/erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json b/erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json index 7bc0deff54..a7c5a37683 100644 --- a/erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json +++ b/erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, "beta": 0, @@ -12,6 +13,7 @@ "engine": "InnoDB", "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 1, "bold": 0, "collapsible": 0, @@ -42,6 +44,39 @@ "unique": 0 }, { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "depends_on": "eval:doc.docstatus >= 1", + "fieldname": "email_sent", + "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": "Email Sent", + "length": 0, + "no_copy": 1, + "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -72,6 +107,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -102,6 +138,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -130,6 +167,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 1, "collapsible": 0, @@ -160,6 +198,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -190,6 +229,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 1, "bold": 0, "collapsible": 0, @@ -219,17 +259,17 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "idx": 0, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-02-17 16:42:57.254211", + "modified": "2017-07-24 06:52:19.542717", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation Supplier", From de54f3019f6f6f6015be4d05dfb9021482a5aafa Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 25 Jul 2017 06:35:07 +0100 Subject: [PATCH 10/31] use api to get default cost center for chosen company (#10067) --- .../doctype/journal_entry/journal_entry.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 577c77f958..9047a4edcc 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -43,8 +43,26 @@ frappe.ui.form.on("Journal Entry", { $.each(frm.doc.accounts || [], function(i, row) { erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name); }) + }, + + company: function(frm) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Company", + filters: {"name": frm.doc.company}, + fieldname: "cost_center" + }, + callback: function(r){ + if(r.message){ + $.each(frm.doc.accounts || [], function(i, jvd) { + frappe.model.set_value(jvd.doctype, jvd.name, "cost_center", r.message.cost_center); + }); + } + } + }); } -}) +}); erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ onload: function() { From 13abada526857c01fd44776cdac61fec82b03d03 Mon Sep 17 00:00:00 2001 From: ci2014 Date: Tue, 25 Jul 2017 07:36:03 +0200 Subject: [PATCH 11/31] Update email-account.md (#10066) * Update email-account.md Add information to conditional import * Add files via upload --- .../email/email-account-incoming-conditions.png | Bin 0 -> 54203 bytes .../manual/en/setting-up/email/email-account.md | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 erpnext/docs/assets/img/setup/email/email-account-incoming-conditions.png diff --git a/erpnext/docs/assets/img/setup/email/email-account-incoming-conditions.png b/erpnext/docs/assets/img/setup/email/email-account-incoming-conditions.png new file mode 100644 index 0000000000000000000000000000000000000000..61a86f565d7064e8a98f0584fd92156fbab7c6ee GIT binary patch literal 54203 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

= zX>0gH5TAEfDe6UZ0w^nEr11}c@oWAkXQyC$jD0=q1mwd=fh2wPl0R5)Y_FV0YMpBy z-FfUUY)@YrVHb355ofIE%kE^!fOB&+03qYw_0K~`?yD3%h>*btUN9(e>Xv>gBQN;~ zN|MNx5v>Y+G1q!6*oCC+`4K~GvJFZg(C%TPnWq;KtY7kLJ+K91E;Se+zyrNd@Uah9 zLc&?+8$<65TnQSs=?B3M&jVy3m1zkSzPp7Q%!Vvq6Nzg@8};9J=|jtyN<6avNeFus zA-)$q_Z|9qM*n^Qt3irT7+#9A9@av4e=l)|u*{KywKoyf+Yi=ABox_i(p6#jrg2l|e>Zo1LVwS`gW?PN jn)}~=@c-XT|NgB5!Rt$w$QMSi_um(Bd9ljR`T_qBwC9!Y literal 0 HcmV?d00001 diff --git a/erpnext/docs/user/manual/en/setting-up/email/email-account.md b/erpnext/docs/user/manual/en/setting-up/email/email-account.md index ad448b0ae2..d5aca1a006 100644 --- a/erpnext/docs/user/manual/en/setting-up/email/email-account.md +++ b/erpnext/docs/user/manual/en/setting-up/email/email-account.md @@ -26,6 +26,12 @@ To setup an incoming Email Account, check on **Enable Incoming** and set your PO Incoming EMail +### Setting Import Conditions for Email Import + +Email Accounts allows you to set conditions according to the data of the incoming emails. The email will be imported to ERPNext only if the all conditions are true. For example if you want to import an email if the subject is "Some important email", you put doc.subject == "Some important email" in the conditions textbox. You can also set more complex conditions by combining them, as shown on the following screenshot. + +Incoming EMail Conditions + ### How ERPNext handles replies In ERPNext when you send an email to a contact like a customer, the sender will be the user who sent the email. In the **Reply-To** property, the Email Address will be of the default incoming account (like `replies@yourcompany.com`). ERPNext will automatically extract these emails from the incoming account and tag it to the relevant communication From d65b4b4238de3beb63589fd5517ff97e20bbd4ad Mon Sep 17 00:00:00 2001 From: KanchanChauhan Date: Tue, 25 Jul 2017 11:39:46 +0530 Subject: [PATCH 12/31] Patch was faling, reload doc was missing (#10072) --- erpnext/patches/v8_4/make_scorecard_records.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v8_4/make_scorecard_records.py b/erpnext/patches/v8_4/make_scorecard_records.py index 37789d711a..73afa277b4 100644 --- a/erpnext/patches/v8_4/make_scorecard_records.py +++ b/erpnext/patches/v8_4/make_scorecard_records.py @@ -2,8 +2,10 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +import frappe from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import make_default_records def execute(): - + frappe.reload_doc('buying', 'doctype', 'supplier_scorecard_variable') + frappe.reload_doc('buying', 'doctype', 'supplier_scorecard_standing') make_default_records() \ No newline at end of file From 1c6828e5d67c884d2af09b756bcf40ba8e8f51fd Mon Sep 17 00:00:00 2001 From: Zarrar Date: Tue, 25 Jul 2017 11:45:39 +0530 Subject: [PATCH 13/31] [UI Tests] School Academic Year test (#10049) --- .../academic_year/test_academic_year.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 erpnext/schools/doctype/academic_year/test_academic_year.js diff --git a/erpnext/schools/doctype/academic_year/test_academic_year.js b/erpnext/schools/doctype/academic_year/test_academic_year.js new file mode 100644 index 0000000000..5123b6e454 --- /dev/null +++ b/erpnext/schools/doctype/academic_year/test_academic_year.js @@ -0,0 +1,24 @@ +// Testing Setup Module in Schools +QUnit.module('setup'); + +// Testing Academic Year Testing option +QUnit.test('test academic year', function(assert){ + assert.expect(3); + let done = assert.async(); + frappe.run_serially([ + () => { + return frappe.tests.make('Academic Year', [ + {academic_year_name: '2015-16'}, + {year_start_date: '2015-07-20'}, + {year_end_date:'2016-06-20'}, + ]); + }, + + () => { + assert.ok(cur_frm.doc.academic_year_name=='2015-16'); + assert.ok(cur_frm.doc.year_start_date=='2015-07-20'); + assert.ok(cur_frm.doc.year_end_date=='2016-06-20'); + }, + () => done() + ]); +}); \ No newline at end of file From b5ec8381a6169db410d7a2e627c345e8879eb8fa Mon Sep 17 00:00:00 2001 From: Zarrar Date: Tue, 25 Jul 2017 11:46:34 +0530 Subject: [PATCH 14/31] [UI Tests] School Academic Term test (#10050) --- .../academic_term/test_academic_term.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 erpnext/schools/doctype/academic_term/test_academic_term.js diff --git a/erpnext/schools/doctype/academic_term/test_academic_term.js b/erpnext/schools/doctype/academic_term/test_academic_term.js new file mode 100644 index 0000000000..c74071b7e7 --- /dev/null +++ b/erpnext/schools/doctype/academic_term/test_academic_term.js @@ -0,0 +1,25 @@ +// Testing Setup Module in Schools +QUnit.module('setup'); + +// Testing Academic Term option +QUnit.test('test academic term', function(assert){ + assert.expect(4); + let done = assert.async(); + frappe.run_serially([ + () => { + return frappe.tests.make('Academic Term', [ + {academic_year: '2016-17'}, + {term_name: "Semester 1"}, + {term_start_date: '2016-07-20'}, + {term_end_date:'2017-06-20'}, + ]); + }, + () => { + assert.ok(cur_frm.doc.academic_year=='2016-17'); + assert.ok(cur_frm.doc.term_name=='Semester 1'); + assert.ok(cur_frm.doc.term_start_date=='2016-07-20'); + assert.ok(cur_frm.doc.term_end_date=='2017-06-20'); + }, + () => done() + ]); +}); \ No newline at end of file From 4b888b95d06cbf8dadf1dce4d838fc8cdb900976 Mon Sep 17 00:00:00 2001 From: KanchanChauhan Date: Tue, 25 Jul 2017 14:03:01 +0530 Subject: [PATCH 15/31] [Minor] Added filter condition to Customer Query (#10057) --- erpnext/controllers/queries.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 8130af925a..6d69a48ab8 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -60,6 +60,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): # searches for customer def customer_query(doctype, txt, searchfield, start, page_len, filters): + conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") if cust_master_name == "Customer Name": @@ -79,7 +80,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select {fields} from `tabCustomer` where docstatus < 2 and ({scond}) and disabled=0 - {mcond} + {fcond} {mcond} order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999), @@ -88,7 +89,8 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): limit %(start)s, %(page_len)s""".format(**{ "fields": fields, "scond": searchfields, - "mcond": get_match_cond(doctype) + "mcond": get_match_cond(doctype), + "fcond": get_filters_cond(doctype, filters, conditions).replace('%', '%%'), }), { 'txt': "%%%s%%" % txt, '_txt': txt.replace("%", ""), From 4cccdbdbf9df88de0c3b053bfe1eb8c2defe46e2 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 25 Jul 2017 14:05:01 +0530 Subject: [PATCH 16/31] [Fix] Validating price list currency even if price list is not defined (#10056) --- erpnext/stock/get_item_details.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index a3ae0eb121..80ef70805a 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -243,7 +243,8 @@ def get_price_list_rate(args, item_doc, out): if meta.get_field("currency"): validate_price_list(args) - validate_conversion_rate(args, meta) + if args.price_list: + validate_conversion_rate(args, meta) price_list_rate = get_price_list_rate_for(args.price_list, item_doc.name) From 05e51d6c8373f0f6bc3cd721731e59393f2825d1 Mon Sep 17 00:00:00 2001 From: bcornwellmott Date: Tue, 25 Jul 2017 01:40:23 -0700 Subject: [PATCH 17/31] Add Get Suppliers dialog (#10025) * Add Get Suppliers dialog * Commonize code, use depends_on * Update request_for_quotation.js --- .../request_for_quotation.js | 85 +++++++++++++++++++ .../request_for_quotation.json | 40 +++++++-- 2 files changed, 120 insertions(+), 5 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 1c7b5a8e86..558e072cad 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -54,6 +54,91 @@ frappe.ui.form.on("Request for Quotation",{ }, + get_suppliers_button: function (frm) { + var doc = frm.doc; + var dialog = new frappe.ui.Dialog({ + title: __("Get Suppliers"), + fields: [ + { "fieldtype": "Select", "label": __("Get Suppliers By"), + "fieldname": "search_type", + "options": "Tag\nSupplier Type", "reqd": 1 }, + { "fieldtype": "Link", "label": __("Supplier Type"), + "fieldname": "supplier_type", + "options": "Supplier Type", "reqd": 0, + "depends_on": "eval:doc.search_type == 'Supplier Type'"}, + { "fieldtype": "Data", "label": __("Tag"), + "fieldname": "tag", "reqd": 0, + "depends_on": "eval:doc.search_type == 'Tag'" }, + { "fieldtype": "Button", "label": __("Add All Suppliers"), + "fieldname": "add_suppliers", "cssClass": "btn-primary"}, + ] + }); + + dialog.fields_dict.add_suppliers.$input.click(function() { + var args = dialog.get_values(); + if(!args) return; + dialog.hide(); + + //Remove blanks + for (var j = 0; j < frm.doc.suppliers.length; j++) { + if(!frm.doc.suppliers[j].hasOwnProperty("supplier")) { + frm.get_field("suppliers").grid.grid_rows[j].remove(); + } + } + + function load_suppliers(r) { + if(r.message) { + for (var i = 0; i < r.message.length; i++) { + var exists = false; + if (r.message[i].constructor === Array){ + var supplier = r.message[i][0]; + } else { + var supplier = r.message[i].name; + } + + for (var j = 0; j < doc.suppliers.length;j++) { + if (supplier === doc.suppliers[j].supplier) { + exists = true; + } + } + if(!exists) { + var d = frm.add_child('suppliers'); + d.supplier = supplier; + frm.script_manager.trigger("supplier", d.doctype, d.name); + } + } + } + frm.refresh_field("suppliers"); + } + + if (args.search_type === "Tag" && args.tag) { + return frappe.call({ + type: "GET", + method: "frappe.desk.tags.get_tagged_docs", + args: { + "doctype": "Supplier", + "tag": args.tag + }, + callback: load_suppliers + }); + } else if (args.supplier_type) { + return frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Supplier", + order_by: "name", + fields: ["name"], + filters: [["Supplier", "supplier_type", "=", args.supplier_type]] + + }, + callback: load_suppliers + }); + } + }); + dialog.show(); + + }, + make_suppplier_quotation: function(frm) { var doc = frm.doc; var dialog = new frappe.ui.Dialog({ diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index ac345d92aa..44068ce81d 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -25,7 +25,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Series", "length": 0, @@ -59,7 +59,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Company", "length": 0, @@ -156,7 +156,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Date", "length": 0, @@ -236,6 +236,36 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "get_suppliers_button", + "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 Suppliers", + "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 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -406,7 +436,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Message for Supplier", "length": 0, @@ -786,7 +816,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-13 14:29:13.171291", + "modified": "2017-07-21 14:06:46.309322", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", From 49a6b4a4fa84313132f6b271ebda7a46475db485 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 25 Jul 2017 14:12:58 +0530 Subject: [PATCH 18/31] [Fix] RFQ list showing to all supplier in the portal (#10023) --- .../doctype/request_for_quotation/request_for_quotation.py | 5 ++++- erpnext/controllers/website_list_for_contact.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 94dc51bdc7..e9603fbcae 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -100,7 +100,10 @@ class RequestforQuotation(BuyingController): else: contact = frappe.new_doc("Contact") contact.first_name = rfq_supplier.supplier_name or rfq_supplier.supplier - contact.supplier = rfq_supplier.supplier + contact.append('links', { + 'link_doctype': 'Supplier', + 'link_name': rfq_supplier.supplier + }) if not contact.email_id and not contact.user: contact.email_id = user.name diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index b078036cb8..04d1bc5dca 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -30,7 +30,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p else: filters.append((doctype, "docstatus", "=", 1)) - if user != "Guest" and is_website_user(): + if (user != "Guest" and is_website_user()) or doctype == 'Request for Quotation': parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype # find party for this contact customers, suppliers = get_customers_suppliers(parties_doctype, user) From 2ffe87899946102fa448535542278ab5399af765 Mon Sep 17 00:00:00 2001 From: Ashwini Save Date: Tue, 25 Jul 2017 14:16:48 +0530 Subject: [PATCH 19/31] Timeline Title for small resolution Add to knowledge base button. (#9926) * Add to Knowledge Base button class updated to hide for mobile view. * Updated code to avoid multiple occurance of Knowledge Base button while updating comment. --- erpnext/support/doctype/issue/issue.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 2bee422ca5..bef8cb4d2b 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -20,9 +20,11 @@ frappe.ui.form.on("Issue", { timeline_refresh: function(frm) { // create button for "Add to Knowledge Base" if(frappe.model.can_create('Help Article')) { - $('') - .appendTo(frm.timeline.wrapper.find('.comment-header')) + .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details')) .on('click', function() { var content = $(this).parents('.timeline-item:first').find('.timeline-item-content').html(); var doc = frappe.model.get_new_doc('Help Article'); From 08450878f1503e3fdfc203842ff5dfdfc0e66cf2 Mon Sep 17 00:00:00 2001 From: Ameya Shenoy Date: Tue, 25 Jul 2017 15:23:58 +0530 Subject: [PATCH 20/31] [ui-test] added test for bill_of_materials (#10063) * temporary commit for switching branches * [ui-tests] added bill_of_materials test * fixed minor codacy problems * added minor requested changes --- erpnext/manufacturing/doctype/bom/test_bom.js | 62 +++++++++++++++++++ erpnext/tests/ui/tests.txt | 1 + 2 files changed, 63 insertions(+) create mode 100644 erpnext/manufacturing/doctype/bom/test_bom.js diff --git a/erpnext/manufacturing/doctype/bom/test_bom.js b/erpnext/manufacturing/doctype/bom/test_bom.js new file mode 100644 index 0000000000..8187ef3617 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom/test_bom.js @@ -0,0 +1,62 @@ +QUnit.test("test: item", function (assert) { + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + // test item creation + () => frappe.set_route("List", "Item"), + + // Create a BOM for a laptop + () => frappe.tests.make( + "BOM", [ + {item: "Laptop"}, + {quantity: 1}, + {with_operations: 1}, + {operations: [ + [ + {operation: "Assemble CPU"}, + {time_in_mins: 60}, + ], + [ + {operation: "Assemble Keyboard"}, + {time_in_mins: 30}, + ], + [ + {operation: "Assemble Screen"}, + {time_in_mins: 30}, + ] + ]}, + {scrap_items: [ + [ + {item_code: "Scrap item"} + ] + ]}, + {items: [ + [ + {item_code: "CPU"}, + {qty: 1} + ], + [ + {item_code: "Keyboard"}, + {qty: 1} + ], + [ + {item_code: "Screen"}, + {qty: 1} + ] + ]}, + ] + ), + () => cur_frm.savesubmit(), + () => frappe.timeout(1), + () => frappe.click_button('Yes'), + () => frappe.timeout(1), + + () => { + assert.ok(cur_frm.doc.operating_cost + cur_frm.doc.raw_material_cost - + cur_frm.doc.scrap_material_cost == cur_frm.doc.total_cost, 'Total_Cost calculated correctly'); + }, + + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index f7e495625f..047ee9cb92 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -8,3 +8,4 @@ erpnext/manufacturing/doctype/workstation/test_workstation.js erpnext/manufacturing/doctype/operation/test_operation.js erpnext/hr/doctype/holiday_list/test_holiday_list.js erpnext/hr/doctype/branch/test_branch.js +erpnext/manufacturing/doctype/bom/test_bom.js \ No newline at end of file From cda6206c1ff36dcabd91f37e36684b5fb1d585d9 Mon Sep 17 00:00:00 2001 From: Prateeksha Singh Date: Wed, 26 Jul 2017 13:05:48 +0530 Subject: [PATCH 21/31] [minor][wiz] remove header brand image (#10016) --- erpnext/public/js/setup_wizard.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 29ef12ce3d..c9e7235760 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -422,15 +422,6 @@ erpnext.setup.fiscal_years = { frappe.setup.on("before_load", function () { erpnext_slides.map(frappe.setup.add_slide); - - // change header brand - let $brand = $('header .setup-wizard-brand'); - if($brand.find('.erpnext-icon').length === 0) { - $brand.find('.frappe-icon').hide(); - $brand.append(` - ERPNext`); - } }); var test_values_edu = { From b8a4a584e60ef295ae3c218c121109ba45c56ae5 Mon Sep 17 00:00:00 2001 From: Utkarsh Yadav Date: Wed, 26 Jul 2017 13:14:34 +0530 Subject: [PATCH 22/31] [ui test] test for department, designation, company and leave block list (#10085) * added test for required items * minor fixes for travis * name changed * added test for employment type * travis fixes --- erpnext/hr/doctype/branch/test_branch.js | 4 +-- .../hr/doctype/department/test_department.js | 22 +++++++++++++++ .../doctype/designation/test_designation.js | 22 +++++++++++++++ .../employment_type/test_employment_type.js | 21 +++++++++++++++ .../doctype/holiday_list/test_holiday_list.js | 4 +-- .../leave_block_list/test_leave_block_list.js | 27 +++++++++++++++++++ erpnext/setup/doctype/company/test_company.js | 23 ++++++++++++++++ erpnext/tests/ui/tests.txt | 7 ++++- 8 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 erpnext/hr/doctype/department/test_department.js create mode 100644 erpnext/hr/doctype/designation/test_designation.js create mode 100644 erpnext/hr/doctype/employment_type/test_employment_type.js create mode 100644 erpnext/hr/doctype/leave_block_list/test_leave_block_list.js create mode 100644 erpnext/setup/doctype/company/test_company.js diff --git a/erpnext/hr/doctype/branch/test_branch.js b/erpnext/hr/doctype/branch/test_branch.js index 25ea2fc392..446db75d34 100644 --- a/erpnext/hr/doctype/branch/test_branch.js +++ b/erpnext/hr/doctype/branch/test_branch.js @@ -10,12 +10,12 @@ QUnit.test("Test: Branch [HR]", function (assert) { () => frappe.new_doc("Branch"), () => frappe.timeout(1), () => frappe.click_link('Edit in full page'), - () => cur_frm.set_value("branch", "Branch test"), + () => cur_frm.set_value("branch", "Test Branch"), // save form () => cur_frm.save(), () => frappe.timeout(1), - () => assert.equal("Branch test", cur_frm.doc.branch, + () => assert.equal("Test Branch", cur_frm.doc.branch, 'name of branch correctly saved'), () => done() ]); diff --git a/erpnext/hr/doctype/department/test_department.js b/erpnext/hr/doctype/department/test_department.js new file mode 100644 index 0000000000..1c413e928b --- /dev/null +++ b/erpnext/hr/doctype/department/test_department.js @@ -0,0 +1,22 @@ +QUnit.module('hr'); + +QUnit.test("Test: Department [HR]", function (assert) { + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + // test department creation + () => frappe.set_route("List", "Department", "List"), + () => frappe.new_doc("Department"), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("department_name", "Test Department"), + () => cur_frm.set_value("leave_block_list", "Test Leave block list"), + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + () => assert.equal("Test Department", cur_frm.doc.department_name, + 'name of department correctly saved'), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/designation/test_designation.js b/erpnext/hr/doctype/designation/test_designation.js new file mode 100644 index 0000000000..a01287775e --- /dev/null +++ b/erpnext/hr/doctype/designation/test_designation.js @@ -0,0 +1,22 @@ +QUnit.module('hr'); + +QUnit.test("Test: Designation [HR]", function (assert) { + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + // test designation creation + () => frappe.set_route("List", "Designation", "List"), + () => frappe.new_doc("Designation"), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("designation_name", "Test Designation"), + () => cur_frm.set_value("description", "This designation is just for testing."), + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + () => assert.equal("Test Designation", cur_frm.doc.designation_name, + 'name of designation correctly saved'), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/employment_type/test_employment_type.js b/erpnext/hr/doctype/employment_type/test_employment_type.js new file mode 100644 index 0000000000..0ddd3e0141 --- /dev/null +++ b/erpnext/hr/doctype/employment_type/test_employment_type.js @@ -0,0 +1,21 @@ +QUnit.module('hr'); + +QUnit.test("Test: Employment type [HR]", function (assert) { + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + // test employment type creation + () => frappe.set_route("List", "Employment Type", "List"), + () => frappe.new_doc("Employment Type"), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("employee_type_name", "Test Employment type"), + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + () => assert.equal("Test Employment type", cur_frm.doc.employee_type_name, + 'name of employment type correctly saved'), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.js b/erpnext/hr/doctype/holiday_list/test_holiday_list.js index ed75285a65..bfcafa9460 100644 --- a/erpnext/hr/doctype/holiday_list/test_holiday_list.js +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.js @@ -10,7 +10,7 @@ QUnit.test("Test: Holiday list [HR]", function (assert) { () => frappe.set_route("List", "Holiday List", "List"), () => frappe.new_doc("Holiday List"), () => frappe.timeout(1), - () => cur_frm.set_value("holiday_list_name", "Holiday list test"), + () => cur_frm.set_value("holiday_list_name", "Test Holiday list"), () => cur_frm.set_value("from_date", date), () => cur_frm.set_value("weekly_off", "Sunday"), // holiday list for sundays () => frappe.click_button('Get Weekly Off Dates'), @@ -18,7 +18,7 @@ QUnit.test("Test: Holiday list [HR]", function (assert) { // save form () => cur_frm.save(), () => frappe.timeout(1), - () => assert.equal("Holiday list test", cur_frm.doc.holiday_list_name, + () => assert.equal("Test Holiday list", cur_frm.doc.holiday_list_name, 'name of holiday list correctly saved'), // check if holiday list contains correct days diff --git a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.js b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.js new file mode 100644 index 0000000000..1241d3d5b6 --- /dev/null +++ b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.js @@ -0,0 +1,27 @@ +QUnit.module('hr'); + +QUnit.test("Test: Leave block list [HR]", function (assert) { + assert.expect(1); + let done = assert.async(); + let today_date = frappe.datetime.nowdate(); + + frappe.run_serially([ + // test leave block list creation + () => frappe.set_route("List", "Leave Block List", "List"), + () => frappe.new_doc("Leave Block List"), + () => frappe.timeout(1), + () => cur_frm.set_value("leave_block_list_name", "Test Leave block list"), + () => cur_frm.set_value("company", "Test Company"), + () => frappe.click_button('Add Row'), + () => { + cur_frm.fields_dict.leave_block_list_dates.grid.grid_rows[0].doc.block_date = today_date; + cur_frm.fields_dict.leave_block_list_dates.grid.grid_rows[0].doc.reason = "Blocked leave test"; + }, + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + () => assert.equal("Test Leave block list", cur_frm.doc.leave_block_list_name, + 'name of blocked leave list correctly saved'), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/setup/doctype/company/test_company.js b/erpnext/setup/doctype/company/test_company.js new file mode 100644 index 0000000000..afa3296e89 --- /dev/null +++ b/erpnext/setup/doctype/company/test_company.js @@ -0,0 +1,23 @@ +QUnit.module('setup'); + +QUnit.test("Test: Company [SetUp]", function (assert) { + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + // test company creation + () => frappe.set_route("List", "Company", "List"), + () => frappe.new_doc("Company"), + () => frappe.timeout(1), + () => cur_frm.set_value("company_name", "Test Company"), + () => cur_frm.set_value("abbr", "TC"), + () => cur_frm.set_value("domain", "Services"), + () => cur_frm.set_value("default_currency", "INR"), + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + () => assert.equal("Test Company", cur_frm.doc.company_name, + 'name of company correctly saved'), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index 047ee9cb92..7c2a76bba8 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -1,4 +1,5 @@ erpnext/tests/ui/make_fixtures.js #long +erpnext/setup/doctype/company/test_company.js erpnext/accounts/doctype/account/test_account.js erpnext/crm/doctype/lead/test_lead.js erpnext/crm/doctype/opportunity/test_opportunity.js @@ -6,6 +7,10 @@ erpnext/selling/doctype/quotation/test_quotation.js erpnext/crm/doctype/item/test_item.js erpnext/manufacturing/doctype/workstation/test_workstation.js erpnext/manufacturing/doctype/operation/test_operation.js +erpnext/manufacturing/doctype/bom/test_bom.js erpnext/hr/doctype/holiday_list/test_holiday_list.js erpnext/hr/doctype/branch/test_branch.js -erpnext/manufacturing/doctype/bom/test_bom.js \ No newline at end of file +erpnext/hr/doctype/leave_block_list/test_leave_block_list.js +erpnext/hr/doctype/department/test_department.js +erpnext/hr/doctype/designation/test_designation.js +erpnext/hr/doctype/employment_type/test_employment_type.js \ No newline at end of file From d0109a6fc0b25801d1cd698c3606fe48722fb234 Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Wed, 26 Jul 2017 16:29:22 +0530 Subject: [PATCH 23/31] [minor] enable all roles and domain before running tests cases (#10108) * [minor] enable all roles and domain before running tests cases * Update utils.py --- erpnext/setup/utils.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 93dad165de..086235e1cb 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -50,6 +50,7 @@ def before_tests(): frappe.db.sql("delete from `tabItem Price`") frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) + enable_all_roles_and_domains() frappe.db.commit() @@ -94,3 +95,29 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None): except: frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date)) return 0.0 + +def enable_all_roles_and_domains(): + """ enable all roles and domain for testing """ + roles = frappe.get_list("Role", filters={"disabled": 1}) + for role in roles: + _role = frappe.get_doc("Role", role.get("name")) + _role.disabled = 0 + _role.flags.ignore_mandatory = True + _role.flags.ignore_permissions = True + _role.save() + + # add all roles to users + user = frappe.get_doc("User", "test@erpnext.com") + user.add_roles(*[role.get("name") for role in roles]) + + domains = frappe.get_list("Domain") + if not domains: + return + + domain_settings = frappe.get_doc("Domain Settings", "Domain Settings") + domain_settings.set("active_domains", []) + for domain in domains: + row = domain_settings.append("active_domains", {}) + row.domain=domain.get("name") + + domain_settings.save() From 40937083bfb5c336d5d012089ec5bd1935524066 Mon Sep 17 00:00:00 2001 From: Utkarsh Goswami Date: Wed, 26 Jul 2017 16:31:08 +0530 Subject: [PATCH 24/31] [UI-Test Project] To check a billing cost of a project with multiple tasks (#10084) * Updated test for Project Timesheet with multiple tasks * Updated test for Project Timesheet with multiple tasks * Updated test for Project Timesheet with multiple tasks * updated: * Updated * Updated --- .../doctype/project/project_timesheet.js | 91 +++++++++++++++++++ erpnext/tests/ui/tests.txt | 5 +- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 erpnext/projects/doctype/project/project_timesheet.js diff --git a/erpnext/projects/doctype/project/project_timesheet.js b/erpnext/projects/doctype/project/project_timesheet.js new file mode 100644 index 0000000000..32df04ff98 --- /dev/null +++ b/erpnext/projects/doctype/project/project_timesheet.js @@ -0,0 +1,91 @@ + +QUnit.test("test project", function(assert) { + assert.expect(6); + let done = assert.async(); + var task_title = ["Documentation","Implementation","Testing"]; + + // To create a timesheet with different tasks and costs + let timesheet = (title,start_time,end_time,bill_rate,cost_rate) => { + return frappe.run_serially([ + () => frappe.db.get_value('Task', {'subject': title}, 'name'), + (task) => { + // Creating timesheet for a project + return frappe.tests.make('Timesheet', [ + {time_logs:[ + [ + {activity_type: 'Communication'}, + {from_time: start_time}, + {to_time: end_time}, + {hours: 2}, + {project: 'Test App'}, + {task: task.name}, + {billable: '1'}, + {billing_rate: bill_rate}, + {costing_rate: cost_rate} + ] + ]} + ]); + }, + // To check if a correct billable and costing amount is calculated for every task + () => { + if(title=== 'Documentation') + { + assert.ok(cur_frm.get_field('total_billable_amount').get_value()==20, + 'Billable amount for Documentation task is correctly calculated'); + assert.ok(cur_frm.get_field('total_costing_amount').get_value()==16, + 'Costing amount for Documentation task is correctly calculated'); + } + if(title=== 'Implementation') + { + assert.ok(cur_frm.get_field('total_billable_amount').get_value()==40, + 'Billable amount for Implementation task is correctly calculated'); + assert.ok(cur_frm.get_field('total_costing_amount').get_value()==32, + 'Costing amount for Implementation task is correctly calculated'); + } + if(title=== 'Testing') + { + assert.ok(cur_frm.get_field('total_billable_amount').get_value()==60, + 'Billable amount for Testing task correctly calculated'); + assert.ok(cur_frm.get_field('total_costing_amount').get_value()==50, + 'Costing amount for Testing task is correctly calculated'); + } + }, + ]); + }; + frappe.run_serially([ + () => { + // Creating project with task + return frappe.tests.make('Project', [ + { project_name: 'Test App'}, + { expected_start_date: '2017-07-22'}, + { expected_end_date: '2017-09-22'}, + { estimated_costing: '10,000.00'}, + { tasks:[ + [ + {title: 'Documentation'}, + {start_date: '2017-07-24'}, + {end_date: '2017-07-31'}, + {description: 'To make a proper documentation defining requirements etc'} + ], + [ + {title: 'Implementation'}, + {start_date: '2017-08-01'}, + {end_date: '2017-08-01'}, + {description: 'Writing algorithms and to code the functionalities'} + ], + [ + {title: 'Testing'}, + {start_date: '2017-08-01'}, + {end_date: '2017-08-15'}, + {description: 'To make the test cases and test the functionalities'} + ] + ]} + ]); + }, + // Creating Timesheet with different tasks + () => timesheet(task_title[0],'2017-07-24 13:00:00','2017-07-24 13:00:00',10,8), + () => timesheet(task_title[1],'2017-07-25 13:00:00','2017-07-25 15:00:00',20,16), + () => timesheet(task_title[2],'2017-07-26 13:00:00','2017-07-26 15:00:00',30,25), + () => done() + ]); +}); diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index 7c2a76bba8..1c429f2a67 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -10,7 +10,10 @@ erpnext/manufacturing/doctype/operation/test_operation.js erpnext/manufacturing/doctype/bom/test_bom.js erpnext/hr/doctype/holiday_list/test_holiday_list.js erpnext/hr/doctype/branch/test_branch.js +erpnext/manufacturing/doctype/bom/test_bom.js +erpnext/projects/doctype/project/project_timesheet.js erpnext/hr/doctype/leave_block_list/test_leave_block_list.js erpnext/hr/doctype/department/test_department.js erpnext/hr/doctype/designation/test_designation.js -erpnext/hr/doctype/employment_type/test_employment_type.js \ No newline at end of file +erpnext/hr/doctype/employment_type/test_employment_type.js + From 53b877bd8fd83860bde164dda69d40138f02bb0b Mon Sep 17 00:00:00 2001 From: Utkarsh Yadav Date: Wed, 26 Jul 2017 16:32:16 +0530 Subject: [PATCH 25/31] [ui test] Employee in HR (#10103) * added test for employee * names changed * path added in tests.txt --- erpnext/hr/doctype/employee/test_employee.js | 38 ++++++++++++++++++++ erpnext/tests/ui/tests.txt | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 erpnext/hr/doctype/employee/test_employee.js diff --git a/erpnext/hr/doctype/employee/test_employee.js b/erpnext/hr/doctype/employee/test_employee.js new file mode 100644 index 0000000000..64fde8ee1a --- /dev/null +++ b/erpnext/hr/doctype/employee/test_employee.js @@ -0,0 +1,38 @@ +QUnit.module('hr'); + +QUnit.test("Test: Employee [HR]", function (assert) { + assert.expect(3); + let done = assert.async(); + let today_date = frappe.datetime.nowdate(); + + frappe.run_serially([ + // test employee creation + () => frappe.set_route("List", "Employee", "List"), + () => frappe.new_doc("Employee"), + () => frappe.timeout(1), + () => cur_frm.set_value("employee_name", "Test Employee"), + () => cur_frm.set_value("salutation", "Ms"), + () => cur_frm.set_value("date_of_joining", frappe.datetime.add_months(today_date, -2)), // joined 2 month from now + () => cur_frm.set_value("date_of_birth", frappe.datetime.add_months(today_date, -240)), // age is 20 years + () => cur_frm.set_value("employment_type", "Test Employment type"), + () => cur_frm.set_value("holiday_list", "Test Holiday list"), + () => cur_frm.set_value("branch", "Test Branch"), + () => cur_frm.set_value("department", "Test Department"), + () => cur_frm.set_value("designation", "Test Designation"), + () => frappe.click_button('Add Row'), + () => cur_frm.fields_dict.leave_approvers.grid.grid_rows[0].doc.leave_approver="Administrator", + // save data + () => cur_frm.save(), + () => frappe.timeout(1), + // check name of employee + () => assert.equal("Test Employee", cur_frm.doc.employee_name, + 'name of employee correctly saved'), + // check auto filled gender according to salutation + () => assert.equal("Female", cur_frm.doc.gender, + 'gender correctly saved as per salutation'), + // check auto filled retirement date [60 years from DOB] + () => assert.equal(frappe.datetime.add_months(today_date, 480), cur_frm.doc.date_of_retirement, // 40 years from now + 'retirement date correctly saved as per date of birth'), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index 1c429f2a67..0025e9fe42 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -16,4 +16,4 @@ erpnext/hr/doctype/leave_block_list/test_leave_block_list.js erpnext/hr/doctype/department/test_department.js erpnext/hr/doctype/designation/test_designation.js erpnext/hr/doctype/employment_type/test_employment_type.js - +erpnext/hr/doctype/employee/test_employee.js \ No newline at end of file From 7800bd89dc579c5078c61c1f59accec62e7a4733 Mon Sep 17 00:00:00 2001 From: KanchanChauhan Date: Wed, 26 Jul 2017 17:55:25 +0530 Subject: [PATCH 26/31] Added Project Type new doctype to be added as link field for Project Type field in project instead of select field (#9929) --- erpnext/config/projects.py | 5 + erpnext/patches.txt | 1 + .../update_existing_data_in_project_type.py | 19 ++ erpnext/projects/doctype/project/project.json | 6 +- .../projects/doctype/project_type/__init__.py | 0 .../doctype/project_type/project_type.js | 6 + .../doctype/project_type/project_type.json | 163 ++++++++++++++++++ .../doctype/project_type/project_type.py | 13 ++ .../doctype/project_type/test_project_type.js | 23 +++ .../doctype/project_type/test_project_type.py | 9 + .../setup/setup_wizard/install_fixtures.py | 4 + 11 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 erpnext/patches/v8_5/update_existing_data_in_project_type.py create mode 100644 erpnext/projects/doctype/project_type/__init__.py create mode 100644 erpnext/projects/doctype/project_type/project_type.js create mode 100644 erpnext/projects/doctype/project_type/project_type.json create mode 100644 erpnext/projects/doctype/project_type/project_type.py create mode 100644 erpnext/projects/doctype/project_type/test_project_type.js create mode 100644 erpnext/projects/doctype/project_type/test_project_type.py diff --git a/erpnext/config/projects.py b/erpnext/config/projects.py index 1a70aee160..a8514b26ff 100644 --- a/erpnext/config/projects.py +++ b/erpnext/config/projects.py @@ -17,6 +17,11 @@ def get_data(): "name": "Task", "description": _("Project activity / task."), }, + { + "type": "doctype", + "name": "Project Type", + "description": _("Define Project type."), + }, { "type": "report", "route": "List/Task/Gantt", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 00f395fe8c..2aabfc646f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -426,3 +426,4 @@ erpnext.patches.v8_4.make_scorecard_records erpnext.patches.v8_1.set_delivery_date_in_so_item erpnext.patches.v8_5.fix_tax_breakup_for_non_invoice_docs erpnext.patches.v8_5.remove_quotations_route_in_sidebar +erpnext.patches.v8_5.update_existing_data_in_project_type diff --git a/erpnext/patches/v8_5/update_existing_data_in_project_type.py b/erpnext/patches/v8_5/update_existing_data_in_project_type.py new file mode 100644 index 0000000000..497da0602e --- /dev/null +++ b/erpnext/patches/v8_5/update_existing_data_in_project_type.py @@ -0,0 +1,19 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("projects", "doctype", "project_type") + frappe.reload_doc("projects", "doctype", "project") + + project_types = ["Internal", "External", "Other"] + + for project_type in project_types: + if not frappe.db.exists("Project Type", project_type): + p_type = frappe.get_doc({ + "doctype": "Project Type", + "project_type": project_type + }) + p_type.insert() \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 28eb730597..101e4ff1c2 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -84,7 +84,7 @@ "collapsible": 0, "columns": 0, "fieldname": "project_type", - "fieldtype": "Select", + "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -97,7 +97,7 @@ "no_copy": 0, "oldfieldname": "project_type", "oldfieldtype": "Data", - "options": "Internal\nExternal\nOther", + "options": "Project Type", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -1255,7 +1255,7 @@ "issingle": 0, "istable": 0, "max_attachments": 4, - "modified": "2017-07-19 14:36:20.857673", + "modified": "2017-07-26 14:36:20.857673", "modified_by": "Administrator", "module": "Projects", "name": "Project", diff --git a/erpnext/projects/doctype/project_type/__init__.py b/erpnext/projects/doctype/project_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/projects/doctype/project_type/project_type.js b/erpnext/projects/doctype/project_type/project_type.js new file mode 100644 index 0000000000..a1f941fe14 --- /dev/null +++ b/erpnext/projects/doctype/project_type/project_type.js @@ -0,0 +1,6 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Project Type', { + +}); \ No newline at end of file diff --git a/erpnext/projects/doctype/project_type/project_type.json b/erpnext/projects/doctype/project_type/project_type.json new file mode 100644 index 0000000000..b9a5b54e8b --- /dev/null +++ b/erpnext/projects/doctype/project_type/project_type.json @@ -0,0 +1,163 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "field:project_type", + "beta": 0, + "creation": "2017-07-18 13:32:46.031115", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "project_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": "Project Type", + "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, + "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": 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 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-26 17:19:30.126346", + "modified_by": "Administrator", + "module": "Projects", + "name": "Project Type", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "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": 0, + "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Projects Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Projects User", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 1, + "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 +} \ No newline at end of file diff --git a/erpnext/projects/doctype/project_type/project_type.py b/erpnext/projects/doctype/project_type/project_type.py new file mode 100644 index 0000000000..f46876eda2 --- /dev/null +++ b/erpnext/projects/doctype/project_type/project_type.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document +import frappe +from frappe import _ + +class ProjectType(Document): + def on_trash(self): + if self.name == "External": + frappe.throw(_("You cannot delete Project Type 'External'")) \ No newline at end of file diff --git a/erpnext/projects/doctype/project_type/test_project_type.js b/erpnext/projects/doctype/project_type/test_project_type.js new file mode 100644 index 0000000000..c2198c452c --- /dev/null +++ b/erpnext/projects/doctype/project_type/test_project_type.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Project Type", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('Project Type', [ + // insert a new Project Type + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/projects/doctype/project_type/test_project_type.py b/erpnext/projects/doctype/project_type/test_project_type.py new file mode 100644 index 0000000000..ee23390f53 --- /dev/null +++ b/erpnext/projects/doctype/project_type/test_project_type.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestProjectType(unittest.TestCase): + pass diff --git a/erpnext/setup/setup_wizard/install_fixtures.py b/erpnext/setup/setup_wizard/install_fixtures.py index 1301d33856..f139d92f35 100644 --- a/erpnext/setup/setup_wizard/install_fixtures.py +++ b/erpnext/setup/setup_wizard/install_fixtures.py @@ -185,6 +185,10 @@ def install(country=None): {'doctype': "Party Type", "party_type": "Supplier"}, {'doctype': "Party Type", "party_type": "Employee"}, + {'doctype': "Project Type", "project_type": "Internal"}, + {'doctype': "Project Type", "project_type": "External"}, + {'doctype': "Project Type", "project_type": "Other"}, + {"doctype": "Offer Term", "offer_term": _("Date of Joining")}, {"doctype": "Offer Term", "offer_term": _("Annual Salary")}, {"doctype": "Offer Term", "offer_term": _("Probationary Period")}, From 1d9fd9aa5209fa04fc7c913e85ef21e4caf6431c Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Wed, 26 Jul 2017 18:19:41 +0530 Subject: [PATCH 27/31] [minor][wiz] fix company image alignment (#10114) (#10117) --- erpnext/public/js/setup_wizard.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index c9e7235760..d551885700 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -46,13 +46,15 @@ var erpnext_slides = [ fieldtype: "Attach Image", fieldname: "attach_logo", label: __("Attach Logo"), description: __("100px by 100px"), - is_private: 0 + is_private: 0, + align: 'center' }, { fieldname: 'company_name', label: frappe.setup.domain === 'Education' ? __('Institute Name') : __('Company Name'), - fieldtype: 'Data', reqd: 1 + fieldtype: 'Data', + reqd: 1 }, { fieldname: 'company_abbr', From 8579dd1d78d75429e88707d2f1fb62f4c4884872 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 27 Jul 2017 11:21:54 +0530 Subject: [PATCH 28/31] [minor] If customer has removed getting an error (#10107) --- erpnext/accounts/doctype/tax_rule/tax_rule.js | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.js b/erpnext/accounts/doctype/tax_rule/tax_rule.js index 935ea62b67..16041a850d 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.js +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.js @@ -20,20 +20,22 @@ frappe.ui.form.on("Tax Rule", "refresh", function(frm) { }) frappe.ui.form.on("Tax Rule", "customer", function(frm) { - frappe.call({ - method:"erpnext.accounts.doctype.tax_rule.tax_rule.get_party_details", - args: { - "party": frm.doc.customer, - "party_type": "customer" - }, - callback: function(r) { - if(!r.exc) { - $.each(r.message, function(k, v) { - frm.set_value(k, v); - }); + if(frm.doc.customer) { + frappe.call({ + method:"erpnext.accounts.doctype.tax_rule.tax_rule.get_party_details", + args: { + "party": frm.doc.customer, + "party_type": "customer" + }, + callback: function(r) { + if(!r.exc) { + $.each(r.message, function(k, v) { + frm.set_value(k, v); + }); + } } - } - }); + }); + } }); frappe.ui.form.on("Tax Rule", "supplier", function(frm) { From 317888211a10ccb52862f622fc17a7a8b94cd267 Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Tue, 1 Aug 2017 11:06:41 +0530 Subject: [PATCH 29/31] merged hotfix branch into staging (#10191) * [Fix] Error in sales invoice and POS if customer group not defined in the customer (#10148) * Revert "[Fix] Error in sales invoice and POS if customer group not defined in the customer (#10148)" (#10159) This reverts commit 4d2e782e42e9875429b5c55934c41dbc5d1d7b20. * [Fix] Unable to save asset because of float error issue (#10157) * bumped to version 8.6.4 * [Fix] Error in sales invoice and POS if customer group not defined in the customer (#10160) * Set billing hours to 0 in timesheet #9535 (#10139) * `update_billing_hours` to use flt not cint * if not billable, reset billable hours * if not billable, reset time rates * test * [Fix] Timesheet Company Issue * Added delivery date in SO parent form. Fixes #10104 (#10155) * Added delivery date in SO parent form. Fixes #10104 * UI tests for sales order delivery date * bumped to version 8.6.5 --- erpnext/__init__.py | 2 +- erpnext/accounts/doctype/asset/asset.py | 5 +- .../doctype/pos_profile/pos_profile.json | 222 ++++++++++++------ .../doctype/pos_profile/pos_profile.py | 8 + .../doctype/pos_profile/test_pos_profile.js | 23 ++ .../doctype/pos_profile/test_pos_profile.py | 1 + erpnext/accounts/doctype/sales_invoice/pos.py | 20 +- erpnext/accounts/page/pos/pos.js | 2 + erpnext/accounts/party.py | 5 +- .../tests/test_recurring_document.py | 3 +- erpnext/demo/user/sales.py | 3 +- .../production_order/production_order.py | 5 +- .../production_order/test_production_order.py | 3 +- erpnext/patches.txt | 4 +- .../v8_1/set_delivery_date_in_so_item.py | 10 +- .../update_customer_group_in_POS_profile.py | 8 + erpnext/patches/v8_6/__init__.py | 0 .../v8_6/update_timesheet_company_from_PO.py | 15 ++ .../doctype/timesheet/test_timesheet.py | 16 +- .../projects/doctype/timesheet/timesheet.js | 21 +- .../projects/doctype/timesheet/timesheet.py | 16 +- .../doctype/quotation/test_quotation.py | 6 +- .../doctype/sales_order/sales_order.js | 20 +- .../doctype/sales_order/sales_order.json | 12 +- .../doctype/sales_order/sales_order.py | 27 ++- .../doctype/sales_order/sales_order_list.js | 6 +- .../doctype/sales_order/test_sales_order.js | 59 +++++ .../doctype/sales_order/test_sales_order.py | 3 +- .../sales_order_item/sales_order_item.json | 4 +- erpnext/tests/ui/tests.txt | 3 +- 30 files changed, 408 insertions(+), 124 deletions(-) create mode 100644 erpnext/accounts/doctype/pos_profile/test_pos_profile.js create mode 100644 erpnext/patches/v8_5/update_customer_group_in_POS_profile.py create mode 100644 erpnext/patches/v8_6/__init__.py create mode 100644 erpnext/patches/v8_6/update_timesheet_company_from_PO.py create mode 100644 erpnext/selling/doctype/sales_order/test_sales_order.js diff --git a/erpnext/__init__.py b/erpnext/__init__.py index b0692038f8..833ddff223 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from erpnext.hooks import regional_overrides -__version__ = '8.6.3' +__version__ = '8.6.5' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/accounts/doctype/asset/asset.py b/erpnext/accounts/doctype/asset/asset.py index 986beb76a3..dd1e491680 100644 --- a/erpnext/accounts/doctype/asset/asset.py +++ b/erpnext/accounts/doctype/asset/asset.py @@ -147,8 +147,9 @@ class Asset(Document): accumulated_depreciation_after_full_schedule = \ max([d.accumulated_depreciation_amount for d in self.get("schedules")]) - asset_value_after_full_schedule = (flt(self.gross_purchase_amount) - - flt(accumulated_depreciation_after_full_schedule)) + asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) - + flt(accumulated_depreciation_after_full_schedule), + self.precision('expected_value_after_useful_life')) if self.expected_value_after_useful_life < asset_value_after_full_schedule: frappe.throw(_("Expected value after useful life must be greater than or equal to {0}") diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 93aca1b361..6991da2888 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -721,38 +721,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tc_name", - "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": "Terms and Conditions", - "length": 0, - "no_copy": 0, - "oldfieldname": "tc_name", - "oldfieldtype": "Link", - "options": "Terms and Conditions", - "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 - }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -782,39 +750,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 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": "Territory", - "length": 0, - "no_copy": 0, - "oldfieldname": "territory", - "oldfieldtype": "Link", - "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, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -847,6 +782,38 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "tc_name", + "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": "Terms and Conditions", + "length": 0, + "no_copy": 0, + "oldfieldname": "tc_name", + "oldfieldtype": "Link", + "options": "Terms and Conditions", + "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 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -910,6 +877,129 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer_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": "New Customer 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, + "unique": 0 + }, + { + "allow_bulk_edit": 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": "Territory", + "length": 0, + "no_copy": 0, + "oldfieldname": "territory", + "oldfieldtype": "Link", + "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": 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": "column_break_31", + "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 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "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": 0, + "in_standard_filter": 0, + "label": "Customer Group", + "length": 0, + "no_copy": 0, + "options": "Customer Group", + "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, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -1201,7 +1291,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-16 17:04:33.165676", + "modified": "2017-07-28 03:40:03.253088", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index ef497bfe29..86682d3c2a 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -14,6 +14,7 @@ class POSProfile(Document): self.check_for_duplicate() self.validate_all_link_fields() self.validate_duplicate_groups() + self.validate_customer_territory_group() def check_for_duplicate(self): res = frappe.db.sql("""select name, user from `tabPOS Profile` @@ -48,6 +49,13 @@ class POSProfile(Document): if len(customer_groups) != len(set(customer_groups)): frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group") + def validate_customer_territory_group(self): + if not self.territory: + frappe.throw(_("Territory is Required in POS Profile"), title="Mandatory Field") + + if not self.customer_group: + frappe.throw(_("Customer Group is Required in POS Profile"), title="Mandatory Field") + def before_save(self): set_account_for_mode_of_payment(self) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.js b/erpnext/accounts/doctype/pos_profile/test_pos_profile.js new file mode 100644 index 0000000000..42e5b7f92f --- /dev/null +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: POS Profile", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('POS Profile', [ + // insert a new POS Profile + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 9c6a11487c..534abb65fa 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -42,6 +42,7 @@ def make_pos_profile(): "naming_series": "_T-POS Profile-", "selling_price_list": "_Test Price List", "territory": "_Test Territory", + "customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'), "warehouse": "_Test Warehouse - _TC", "write_off_account": "_Test Write Off - _TC", "write_off_cost_center": "_Test Write Off Cost Center - _TC" diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index f61931a562..3c9de12fc0 100644 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe, json +from frappe import _ from frappe.utils import nowdate from erpnext.setup.utils import get_exchange_rate from frappe.core.doctype.communication.email import make @@ -20,6 +21,7 @@ def get_pos_data(): if pos_profile.get('name'): pos_profile = frappe.get_doc('POS Profile', pos_profile.get('name')) + pos_profile.validate() company_data = get_company_data(doc.company) update_pos_profile_data(doc, pos_profile, company_data) @@ -378,13 +380,27 @@ def add_customer(data): customer_doc.customer_name = data.get('full_name') or data.get('customer') customer_doc.customer_pos_id = data.get('customer_pos_id') customer_doc.customer_type = 'Company' - customer_doc.customer_group = frappe.db.get_single_value('Selling Settings', 'customer_group') - customer_doc.territory = frappe.db.get_single_value('Selling Settings', 'territory') + customer_doc.customer_group = get_customer_group(data) + customer_doc.territory = get_territory(data) customer_doc.flags.ignore_mandatory = True customer_doc.save(ignore_permissions = True) frappe.db.commit() return customer_doc.name +def get_territory(data): + if data.get('territory'): + return data.get('territory') + + return frappe.db.get_single_value('Selling Settings', + 'territory') or _('All Territories') + +def get_customer_group(data): + if data.get('customer_group'): + return data.get('customer_group') + + return frappe.db.get_single_value('Selling Settings', + 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name') + def make_contact(args,customer): if args.get('email_id') or args.get('phone'): name = frappe.db.get_value('Dynamic Link', diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index a5f9b3c286..d69a306670 100644 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -979,6 +979,8 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ get_prompt_details: function() { this.prompt_details = this.customer_doc.get_values(); this.prompt_details['country'] = this.pos_profile_data.country; + this.prompt_details['territory'] = this.pos_profile_data["territory"]; + this.prompt_details['customer_group'] = this.pos_profile_data["customer_group"]; this.prompt_details['customer_pos_id'] = this.customer_doc.fields_dict.customer_pos_id.value; return JSON.stringify(this.prompt_details) }, diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 005abe646d..0f8b92ffc9 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -273,6 +273,7 @@ def get_due_date(posting_date, party_type, party, company): return due_date def get_credit_days(party_type, party, company): + credit_days = 0 if party_type and party: if party_type == "Customer": credit_days_based_on, credit_days, customer_group = \ @@ -282,10 +283,10 @@ def get_credit_days(party_type, party, company): frappe.db.get_value(party_type, party, ["credit_days_based_on", "credit_days", "supplier_type"]) if not credit_days_based_on: - if party_type == "Customer": + if party_type == "Customer" and customer_group: credit_days_based_on, credit_days = \ frappe.db.get_value("Customer Group", customer_group, ["credit_days_based_on", "credit_days"]) - else: + elif party_type == "Supplier" and supplier_type: credit_days_based_on, credit_days = \ frappe.db.get_value("Supplier Type", supplier_type, ["credit_days_based_on", "credit_days"]) diff --git a/erpnext/controllers/tests/test_recurring_document.py b/erpnext/controllers/tests/test_recurring_document.py index e218fa6c9b..d47c5c7701 100644 --- a/erpnext/controllers/tests/test_recurring_document.py +++ b/erpnext/controllers/tests/test_recurring_document.py @@ -27,8 +27,7 @@ def test_recurring_document(obj, test_records): base_doc.set(date_field, today) if base_doc.doctype == "Sales Order": - for d in base_doc.get("items"): - d.set("delivery_date", add_days(today, 15)) + base_doc.set("delivery_date", add_days(today, 15)) # monthly doc1 = frappe.copy_doc(base_doc) diff --git a/erpnext/demo/user/sales.py b/erpnext/demo/user/sales.py index 666e201716..ddd36efc36 100644 --- a/erpnext/demo/user/sales.py +++ b/erpnext/demo/user/sales.py @@ -118,8 +118,7 @@ def make_sales_order(): from erpnext.selling.doctype.quotation.quotation import make_sales_order so = frappe.get_doc(make_sales_order(q)) so.transaction_date = frappe.flags.current_date - for d in so.get("items"): - d.delivery_date = frappe.utils.add_days(frappe.flags.current_date, 10) + so.delivery_date = frappe.utils.add_days(frappe.flags.current_date, 10) so.insert() frappe.db.commit() so.submit() diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 95336137cb..022e9f3d18 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -273,7 +273,7 @@ class ProductionOrder(Document): timesheets = [] plan_days = frappe.db.get_single_value("Manufacturing Settings", "capacity_planning_for_days") or 30 - timesheet = make_timesheet(self.name) + timesheet = make_timesheet(self.name, self.company) timesheet.set('time_logs', []) for i, d in enumerate(self.operations): @@ -575,10 +575,11 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_timesheet(production_order): +def make_timesheet(production_order, company): timesheet = frappe.new_doc("Timesheet") timesheet.employee = "" timesheet.production_order = production_order + timesheet.company = company return timesheet @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index 18aa51d874..1d555f7c0c 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -87,6 +87,7 @@ class TestProductionOrder(unittest.TestCase): name = frappe.db.get_value('Timesheet', {'production_order': prod_order.name}, 'name') time_sheet_doc = frappe.get_doc('Timesheet', name) + self.assertEqual(prod_order.company, time_sheet_doc.company) time_sheet_doc.submit() @@ -107,7 +108,7 @@ class TestProductionOrder(unittest.TestCase): self.assertEqual(prod_order.operations[0].actual_operation_time, 60) self.assertEqual(prod_order.operations[0].actual_operating_cost, 100) - time_sheet_doc1 = make_timesheet(prod_order.name) + time_sheet_doc1 = make_timesheet(prod_order.name, prod_order.company) self.assertEqual(len(time_sheet_doc1.get('time_logs')), 0) time_sheet_doc.cancel() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2aabfc646f..d018968083 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -423,7 +423,9 @@ erpnext.patches.v8_3.set_restrict_to_domain_for_module_def erpnext.patches.v8_1.update_expense_claim_status erpnext.patches.v8_3.update_company_total_sales erpnext.patches.v8_4.make_scorecard_records -erpnext.patches.v8_1.set_delivery_date_in_so_item +erpnext.patches.v8_1.set_delivery_date_in_so_item #2017-07-28 erpnext.patches.v8_5.fix_tax_breakup_for_non_invoice_docs erpnext.patches.v8_5.remove_quotations_route_in_sidebar erpnext.patches.v8_5.update_existing_data_in_project_type +erpnext.patches.v8_5.update_customer_group_in_POS_profile +erpnext.patches.v8_6.update_timesheet_company_from_PO \ No newline at end of file diff --git a/erpnext/patches/v8_1/set_delivery_date_in_so_item.py b/erpnext/patches/v8_1/set_delivery_date_in_so_item.py index 6840424774..963b82a4de 100644 --- a/erpnext/patches/v8_1/set_delivery_date_in_so_item.py +++ b/erpnext/patches/v8_1/set_delivery_date_in_so_item.py @@ -4,10 +4,18 @@ def execute(): frappe.reload_doctype("Sales Order") frappe.reload_doctype("Sales Order Item") - frappe.db.sql("""update `tabSales Order` set final_delivery_date = delivery_date where docstatus=1""") + if "final_delivery_date" in frappe.db.get_table_columns("Sales Order"): + frappe.db.sql(""" + update `tabSales Order` + set delivery_date = final_delivery_date + where (delivery_date is null or delivery_date = '' or delivery_date = '0000-00-00') + and order_type = 'Sales'""") frappe.db.sql(""" update `tabSales Order` so, `tabSales Order Item` so_item set so_item.delivery_date = so.delivery_date where so.name = so_item.parent + and so.order_type = 'Sales' + and (so_item.delivery_date is null or so_item.delivery_date = '' + or so_item.delivery_date = '0000-00-00') """) \ No newline at end of file diff --git a/erpnext/patches/v8_5/update_customer_group_in_POS_profile.py b/erpnext/patches/v8_5/update_customer_group_in_POS_profile.py new file mode 100644 index 0000000000..9a5fef90c8 --- /dev/null +++ b/erpnext/patches/v8_5/update_customer_group_in_POS_profile.py @@ -0,0 +1,8 @@ +import frappe + +def execute(): + frappe.reload_doctype('POS Profile') + customer_group = frappe.db.get_single_value('Selling Settings', 'customer_group') + if customer_group: + frappe.db.sql(""" update `tabPOS Profile` + set customer_group = %s where customer_group is null """, (customer_group)) \ No newline at end of file diff --git a/erpnext/patches/v8_6/__init__.py b/erpnext/patches/v8_6/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/patches/v8_6/update_timesheet_company_from_PO.py b/erpnext/patches/v8_6/update_timesheet_company_from_PO.py new file mode 100644 index 0000000000..5bab961c04 --- /dev/null +++ b/erpnext/patches/v8_6/update_timesheet_company_from_PO.py @@ -0,0 +1,15 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doctype('Timesheet') + company = frappe.get_all('Company') + + #Check more than one company exists + if len(company) > 1: + frappe.db.sql(""" update `tabTimesheet` set `tabTimesheet`.company = + (select company from `tabProduction Order` where name = `tabTimesheet`.production_order) + where production_order is not null and production_order !=''""") \ No newline at end of file diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 1db0610e89..955a2b0fe3 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -14,8 +14,8 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal class TestTimesheet(unittest.TestCase): def test_timesheet_billing_amount(self): - salary_structure = make_salary_structure("_T-Employee-0001") - timesheet = make_timesheet("_T-Employee-0001", simulate = True, billable=1) + make_salary_structure("_T-Employee-0001") + timesheet = make_timesheet("_T-Employee-0001", simulate=True, billable=1) self.assertEquals(timesheet.total_hours, 2) self.assertEquals(timesheet.total_billable_hours, 2) @@ -23,6 +23,16 @@ class TestTimesheet(unittest.TestCase): self.assertEquals(timesheet.time_logs[0].billing_amount, 100) self.assertEquals(timesheet.total_billable_amount, 100) + def test_timesheet_billing_amount_not_billable(self): + make_salary_structure("_T-Employee-0001") + timesheet = make_timesheet("_T-Employee-0001", simulate=True, billable=0) + + self.assertEquals(timesheet.total_hours, 2) + self.assertEquals(timesheet.total_billable_hours, 0) + self.assertEquals(timesheet.time_logs[0].billing_rate, 0) + self.assertEquals(timesheet.time_logs[0].billing_amount, 0) + self.assertEquals(timesheet.total_billable_amount, 0) + def test_salary_slip_from_timesheet(self): salary_structure = make_salary_structure("_T-Employee-0001") timesheet = make_timesheet("_T-Employee-0001", simulate = True, billable=1) @@ -43,7 +53,7 @@ class TestTimesheet(unittest.TestCase): self.assertEquals(timesheet.status, 'Submitted') def test_sales_invoice_from_timesheet(self): - timesheet = make_timesheet("_T-Employee-0001", simulate = True, billable = 1) + timesheet = make_timesheet("_T-Employee-0001", simulate=True, billable=1) sales_invoice = make_sales_invoice(timesheet.name) sales_invoice.customer = "_Test Customer" sales_invoice.due_date = nowdate() diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 441ab1afb0..9b330e7811 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -105,7 +105,9 @@ frappe.ui.form.on("Timesheet Detail", { }, billable: function(frm, cdt, cdn) { - calculate_billing_costing_amount(frm, cdt, cdn) + update_billing_hours(frm, cdt, cdn); + update_time_rates(frm, cdt, cdn); + calculate_billing_costing_amount(frm, cdt, cdn); }, activity_type: function(frm, cdt, cdn) { @@ -148,8 +150,21 @@ var calculate_end_time = function(frm, cdt, cdn) { } } +var update_billing_hours = function(frm, cdt, cdn){ + var child = locals[cdt][cdn]; + if(!child.billable) frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); +} + +var update_time_rates = function(frm, cdt, cdn){ + var child = locals[cdt][cdn]; + if(!child.billable){ + frappe.model.set_value(cdt, cdn, 'billing_rate', 0.0); + frappe.model.set_value(cdt, cdn, 'costing_rate', 0.0); + } +} + var calculate_billing_costing_amount = function(frm, cdt, cdn){ - var child = locals[cdt][cdn] + var child = locals[cdt][cdn]; var billing_amount = 0.0; var costing_amount = 0.0; @@ -160,7 +175,7 @@ var calculate_billing_costing_amount = function(frm, cdt, cdn){ frappe.model.set_value(cdt, cdn, 'billing_amount', billing_amount); frappe.model.set_value(cdt, cdn, 'costing_amount', costing_amount); - calculate_time_and_amount(frm) + calculate_time_and_amount(frm); } var calculate_time_and_amount = function(frm) { diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 95fd420ba4..45d6442d2c 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -46,6 +46,7 @@ class Timesheet(Document): for d in self.get("time_logs"): self.update_billing_hours(d) + self.update_time_rates(d) self.total_hours += flt(d.hours) if d.billable: @@ -61,8 +62,11 @@ class Timesheet(Document): self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount def update_billing_hours(self, args): - if cint(args.billing_hours) == 0: - args.billing_hours = args.hours + if args.billable: + if flt(args.billing_hours) == 0.0: + args.billing_hours = args.hours + else: + args.billing_hours = 0 def set_status(self): self.status = { @@ -263,13 +267,19 @@ class Timesheet(Document): for data in self.time_logs: if data.activity_type and data.billable: rate = get_activity_cost(self.employee, data.activity_type) - hours = data.billing_hours or 0 + hours = data.billing_hours or 0 if rate: data.billing_rate = flt(rate.get('billing_rate')) if flt(data.billing_rate) == 0 else data.billing_rate data.costing_rate = flt(rate.get('costing_rate')) if flt(data.costing_rate) == 0 else data.costing_rate data.billing_amount = data.billing_rate * hours data.costing_amount = data.costing_rate * hours + def update_time_rates(self, ts_detail): + if not ts_detail.billable: + ts_detail.billing_rate = 0.0 + ts_detail.costing_rate = 0.0 + + @frappe.whitelist() def get_projectwise_timesheet_data(project, parent=None): cond = '' diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 7fb40748f0..fa69df418a 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -27,8 +27,7 @@ class TestQuotation(unittest.TestCase): self.assertEquals(sales_order.get("items")[0].prevdoc_docname, quotation.name) self.assertEquals(sales_order.customer, "_Test Customer") - for d in sales_order.get("items"): - d.delivery_date = "2014-01-01" + sales_order.delivery_date = "2014-01-01" sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = "2013-05-12" sales_order.insert() @@ -54,8 +53,7 @@ class TestQuotation(unittest.TestCase): sales_order = make_sales_order(quotation.name) sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = "2016-01-01" - for d in sales_order.get("items"): - d.delivery_date = "2016-01-02" + sales_order.delivery_date = "2016-01-02" sales_order.insert() diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 2e01a08359..80dc4f23ec 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -34,11 +34,20 @@ frappe.ui.form.on("Sales Order", { erpnext.queries.setup_warehouse_query(frm); }, + + delivery_date: function(frm) { + $.each(frm.doc.items || [], function(i, d) { + if(!d.delivery_date) d.delivery_date = frm.doc.delivery_date; + }); + refresh_field("items"); + } }); frappe.ui.form.on("Sales Order Item", { delivery_date: function(frm, cdt, cdn) { - erpnext.utils.copy_value_in_all_row(frm.doc, cdt, cdn, "items", "delivery_date"); + if(!frm.doc.delivery_date) { + erpnext.utils.copy_value_in_all_row(frm.doc, cdt, cdn, "items", "delivery_date"); + } } }); @@ -416,8 +425,13 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( items_add: function(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - this.frm.script_manager.copy_from_first_row("items", row, ["delivery_date"]); + if(doc.delivery_date) { + row.delivery_date = doc.delivery_date; + refresh_field("delivery_date", cdn, "items"); + } else { + this.frm.script_manager.copy_from_first_row("items", row, ["delivery_date"]); + } } }); -$.extend(cur_frm.cscript, new erpnext.selling.SalesOrderController({frm: cur_frm})); +$.extend(cur_frm.cscript, new erpnext.selling.SalesOrderController({frm: cur_frm})); \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 596bf2f9dd..bf2ccf78f8 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -367,23 +367,23 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "final_delivery_date", + "fieldname": "delivery_date", "fieldtype": "Date", - "hidden": 1, + "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": "Final Delivery Date", + "label": "Delivery Date", "length": 0, "no_copy": 1, "permlevel": 0, "precision": "", - "print_hide": 1, + "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 1, + "read_only": 0, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, @@ -3659,7 +3659,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-07-25 13:45:02.965353", + "modified": "2017-07-28 14:03:33.373347", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index fd80dc8662..c6dbd7043b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -101,20 +101,20 @@ class SalesOrder(SellingController): super(SalesOrder, self).validate_order_type() def validate_delivery_date(self): - self.final_delivery_date = None if self.order_type == 'Sales': - for d in self.get("items"): - if not d.delivery_date: - frappe.throw(_("Row #{0}: Please enter Delivery Date against item {1}") - .format(d.idx, d.item_code)) + if not self.delivery_date: + self.delivery_date = max([d.delivery_date for d in self.get("items")]) - if getdate(self.transaction_date) > getdate(d.delivery_date): - frappe.msgprint(_("Expected Delivery Date should be after Sales Order Date"), - indicator='orange', title=_('Warning')) - - if not self.final_delivery_date or \ - (d.delivery_date and getdate(d.delivery_date) > getdate(self.final_delivery_date)): - self.final_delivery_date = d.delivery_date + if self.delivery_date: + for d in self.get("items"): + if not d.delivery_date: + d.delivery_date = self.delivery_date + + if getdate(self.transaction_date) > getdate(d.delivery_date): + frappe.msgprint(_("Expected Delivery Date should be after Sales Order Date"), + indicator='orange', title=_('Warning')) + else: + frappe.throw(_("Please enter Delivery Date")) self.validate_sales_mntc_quotation() @@ -347,6 +347,9 @@ class SalesOrder(SellingController): def on_recurring(self, reference_doc): mcount = month_map[reference_doc.recurring_type] + self.set("delivery_date", get_next_date(reference_doc.delivery_date, mcount, + cint(reference_doc.repeat_on_day_of_month))) + for d in self.get("items"): reference_delivery_date = frappe.db.get_value("Sales Order Item", {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, "delivery_date") diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 0ee9cf34f0..9751935183 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -1,14 +1,14 @@ frappe.listview_settings['Sales Order'] = { - add_fields: ["base_grand_total", "customer_name", "currency", "final_delivery_date", + add_fields: ["base_grand_total", "customer_name", "currency", "delivery_date", "per_delivered", "per_billed", "status", "order_type", "name"], get_indicator: function(doc) { if(doc.status==="Closed"){ return [__("Closed"), "green", "status,=,Closed"]; } else if (doc.order_type !== "Maintenance" - && flt(doc.per_delivered, 2) < 100 && frappe.datetime.get_diff(doc.final_delivery_date) < 0) { + && flt(doc.per_delivered, 2) < 100 && frappe.datetime.get_diff(doc.delivery_date) < 0) { // to bill & overdue - return [__("Overdue"), "red", "per_delivered,<,100|final_delivery_date,<,Today|status,!=,Closed"]; + return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"]; } else if (doc.order_type !== "Maintenance" && flt(doc.per_delivered, 2) < 100 && doc.status!=="Closed") { diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.js b/erpnext/selling/doctype/sales_order/test_sales_order.js new file mode 100644 index 0000000000..87f0e965c2 --- /dev/null +++ b/erpnext/selling/doctype/sales_order/test_sales_order.js @@ -0,0 +1,59 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Sales Order", function (assert) { + assert.expect(2); + let done = assert.async(); + let delivery_date = frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1); + + frappe.run_serially([ + // insert a new Sales Order + () => { + return frappe.tests.make('Sales Order', [ + {customer: "Test Customer 1"}, + {delivery_date: delivery_date}, + {order_type: 'Sales'}, + {items: [ + [ + {"item_code": "Test Product 1"}, + {"qty": 5}, + {'rate': 100}, + ]] + } + ]) + }, + () => { + assert.ok(cur_frm.doc.items[0].delivery_date == delivery_date); + }, + () => frappe.timeout(1), + // make SO without delivery date in parent, + // parent delivery date should be set based on final delivery date entered in item + () => { + return frappe.tests.make('Sales Order', [ + {customer: "Test Customer 1"}, + {order_type: 'Sales'}, + {items: [ + [ + {"item_code": "Test Product 1"}, + {"qty": 5}, + {'rate': 100}, + {'delivery_date': delivery_date} + ], + [ + {"item_code": "Test Product 2"}, + {"qty": 5}, + {'rate': 100}, + {'delivery_date': frappe.datetime.add_days(delivery_date, 5)} + ]] + } + ]) + }, + () => cur_frm.save(), + () => frappe.timeout(1), + () => { + assert.ok(cur_frm.doc.delivery_date == frappe.datetime.add_days(delivery_date, 5)); + }, + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 8c0711870b..ce8c18af41 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -532,8 +532,7 @@ def make_sales_order(**args): "rate": args.rate or 100 }) - for d in so.get("items"): - d.delivery_date = add_days(so.transaction_date, 10) + so.delivery_date = add_days(so.transaction_date, 10) if not args.do_not_save: so.insert() 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 f14f50dedd..47bc1b6077 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -220,7 +220,7 @@ "no_copy": 1, "permlevel": 0, "precision": "", - "print_hide": 0, + "print_hide": 1, "print_hide_if_no_value": 0, "read_only": 0, "remember_last_selected_value": 0, @@ -1963,7 +1963,7 @@ "istable": 1, "max_attachments": 0, "menu_index": 0, - "modified": "2017-07-18 18:26:36.870342", + "modified": "2017-07-28 14:04:04.289428", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt index 0025e9fe42..84af433815 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -16,4 +16,5 @@ erpnext/hr/doctype/leave_block_list/test_leave_block_list.js erpnext/hr/doctype/department/test_department.js erpnext/hr/doctype/designation/test_designation.js erpnext/hr/doctype/employment_type/test_employment_type.js -erpnext/hr/doctype/employee/test_employee.js \ No newline at end of file +erpnext/hr/doctype/employee/test_employee.js +erpnext/selling/doctype/sales_order/test_sales_order.js \ No newline at end of file From 578624db1ff194dd15f06bc0ec1fec64e3caaf90 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 2 Aug 2017 17:44:32 +0530 Subject: [PATCH 30/31] Fix column width in GST Tax Breakup (#10230) --- erpnext/templates/includes/itemised_tax_breakup.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html index 342ce6b292..2ffc8b4b83 100644 --- a/erpnext/templates/includes/itemised_tax_breakup.html +++ b/erpnext/templates/includes/itemised_tax_breakup.html @@ -2,14 +2,12 @@ - {% set i = 0 %} {% for key in headers %} - {% if i==0 %} + {% if loop.first %} {% else %} {% endif %} - {% set i = i + 1 %} {% endfor%} From 478ffb9ae303c96aaca93d9548df9528887aebc0 Mon Sep 17 00:00:00 2001 From: mbauskar Date: Wed, 2 Aug 2017 19:05:51 +0600 Subject: [PATCH 31/31] bumped to version 8.7.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 618d106390..070c93f096 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from erpnext.hooks import regional_overrides -__version__ = '8.6.6' +__version__ = '8.7.0' def get_default_company(user=None): '''Get default company for user'''
{{ key }}{{ key }}