From f16f9c569bca3242b9612ac360a5869f0bf4fcf0 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 8 Apr 2016 17:20:50 +0530 Subject: [PATCH] [enhancement] add status in customer, supplier --- .../doctype/fiscal_year/fiscal_year.py | 10 +-- erpnext/accounts/party_status.py | 72 +++++++++++++++++++ erpnext/buying/doctype/supplier/supplier.json | 28 +++++++- .../buying/doctype/supplier/supplier_list.js | 7 +- erpnext/hooks.py | 13 ++-- erpnext/patches.txt | 1 + erpnext/patches/v7_0/__init__.py | 0 erpnext/patches/v7_0/update_party_status.py | 7 ++ .../selling/doctype/customer/customer.json | 29 +++++++- .../selling/doctype/customer/customer_list.js | 11 ++- .../selling/doctype/customer/test_customer.py | 71 ++++++++++++++---- .../doctype/quotation/test_quotation.py | 18 +++++ 12 files changed, 239 insertions(+), 28 deletions(-) create mode 100644 erpnext/accounts/party_status.py create mode 100644 erpnext/patches/v7_0/__init__.py create mode 100644 erpnext/patches/v7_0/update_party_status.py diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index ce7635400d..c3f399d3d6 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -41,7 +41,7 @@ class FiscalYear(Document): def on_update(self): check_duplicate_fiscal_year(self) - + def validate_overlap(self): existing_fiscal_years = frappe.db.sql("""select name from `tabFiscal Year` where ( @@ -60,18 +60,18 @@ class FiscalYear(Document): for existing in existing_fiscal_years: company_for_existing = frappe.db.sql_list("""select company from `tabFiscal Year Company` where parent=%s""", existing.name) - + overlap = False if not self.get("companies") or not company_for_existing: overlap = True - + for d in self.get("companies"): if d.company in company_for_existing: overlap = True - + if overlap: frappe.throw(_("Year start date or end date is overlapping with {0}. To avoid please set company") - .format(existing.name)) + .format(existing.name), frappe.NameError) @frappe.whitelist() def check_duplicate_fiscal_year(doc): diff --git a/erpnext/accounts/party_status.py b/erpnext/accounts/party_status.py new file mode 100644 index 0000000000..d0f60a5f62 --- /dev/null +++ b/erpnext/accounts/party_status.py @@ -0,0 +1,72 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +from frappe.utils import evaluate_filters +from erpnext.startup.notifications import get_notification_config + +status_depends_on = { + 'Customer': ('Opportunity', 'Quotation', 'Sales Order', 'Sales Invoice', 'Project', 'Issue'), + 'Supplier': ('Supplier Quotation', 'Purchase Order', 'Purchase Invoice') +} + +default_status = { + 'Customer': 'Active', + 'Supplier': None +} + +def notify_status(doc, method): + '''Notify status to customer, supplier''' + + party_type = None + for key, doctypes in status_depends_on.iteritems(): + if doc.doctype in doctypes: + party_type = key + break + + if not party_type: + return + + party = frappe.get_doc(party_type, doc.get(party_type.lower())) + config = get_notification_config().get('for_doctype').get(doc.doctype) + + status = None + if config: + if evaluate_filters(doc, config): + # filters match, passed document is open + status = 'Open' + + if status=='Open': + if party.status != 'Open': + # party not open, make it open + party.status = 'Open' + party.save(ignore_permissions=True) + + else: + if party.status == 'Open': + # may be open elsewhere, check + # default status + party.status = status + update_status(party, ) + +def update_status(doc): + '''Set status as open if there is any open notification''' + config = get_notification_config() + + original_status = doc.status + + doc.status = default_status[doc.doctype] + for doctype in status_depends_on[doc.doctype]: + filters = config.get('for_doctype', {}).get(doctype) or {} + filters[doc.doctype.lower()] = doc.name + if filters: + open_count = frappe.get_all(doctype, fields='count(*) as count', filters=filters) + if open_count[0].count > 0: + doc.status = 'Open' + break + + if doc.status != original_status: + doc.db_set('status', doc.status) diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 46ea98fcc9..bafa1429aa 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -114,6 +114,32 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "\nOpen", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -658,7 +684,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-04-06 05:39:47.329568", + "modified": "2016-04-08 07:43:07.541419", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier_list.js b/erpnext/buying/doctype/supplier/supplier_list.js index d26932c915..acf8e68ee3 100644 --- a/erpnext/buying/doctype/supplier/supplier_list.js +++ b/erpnext/buying/doctype/supplier/supplier_list.js @@ -1,3 +1,8 @@ frappe.listview_settings['Supplier'] = { - add_fields: ["supplier_name", "supplier_type"] + add_fields: ["supplier_name", "supplier_type", 'status'], + get_indicator: function(doc) { + if(doc.status==="Open") { + return [doc.status, "red", "status,=," + doc.status]; + } + } }; diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 72bc40d2fd..1a521c65d7 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -127,15 +127,18 @@ doc_events = { "on_update": "erpnext.hr.doctype.employee.employee.update_user_permissions", "on_update": "erpnext.utilities.doctype.contact.contact.update_contact" }, - "Sales Taxes and Charges Template": { - "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" - }, - "Price List": { + ("Sales Taxes and Charges Template", 'Price List'): { "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" }, "Address": { "validate": "erpnext.shopping_cart.cart.set_customer_in_address" - } + }, + + # bubble transaction notification on master + ('Opportunity', 'Quotation', 'Sales Order', 'Sales Invoice', 'Supplier Quotation', + 'Purchase Order', 'Purchase Invoice', 'Project', 'Issue'): { + 'on_update': 'erpnext.accounts.party_status.notify_status' + } } scheduler_events = { diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5c2376eef8..975d58990b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -260,3 +260,4 @@ erpnext.patches.v6_24.map_customer_address_to_shipping_address_on_po erpnext.patches.v6_27.fix_recurring_order_status erpnext.patches.v6_20x.remove_customer_supplier_roles erpnext.patches.v6_24.rename_item_field +erpnext.patches.v7_0.update_party_status diff --git a/erpnext/patches/v7_0/__init__.py b/erpnext/patches/v7_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/patches/v7_0/update_party_status.py b/erpnext/patches/v7_0/update_party_status.py new file mode 100644 index 0000000000..c9cab95eec --- /dev/null +++ b/erpnext/patches/v7_0/update_party_status.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + for doctype in ('Customer', 'Supplier'): + for doc in frappe.get_all(doctype): + doc = frappe.get_doc(doctype, doc.name) + doc.update_status() \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 6974eb5234..ba7aa24ef9 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -166,6 +166,33 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "Active\nDormant\nOpen", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -927,7 +954,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-04-07 01:25:25.676480", + "modified": "2016-04-08 07:43:01.381976", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/customer/customer_list.js b/erpnext/selling/doctype/customer/customer_list.js index 012d3f81dd..d650b01a1a 100644 --- a/erpnext/selling/doctype/customer/customer_list.js +++ b/erpnext/selling/doctype/customer/customer_list.js @@ -1,3 +1,12 @@ frappe.listview_settings['Customer'] = { - add_fields: ["customer_name", "territory", "customer_group", "customer_type"] + add_fields: ["customer_name", "territory", "customer_group", "customer_type", 'status'], + get_indicator: function(doc) { + color = { + 'Open': 'red', + 'Active': 'green', + 'Dormant', 'dardgrey' + } + return [__(doc.status), color[doc.status], "status,=," + doc.status]; + } + }; diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 1ca5ce7318..85b216bbac 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -11,6 +11,8 @@ from erpnext.exceptions import PartyFrozen, PartyDisabled test_ignore = ["Price List"] +test_dependencies = ['Quotation'] + test_records = frappe.get_test_records('Customer') class TestCustomer(unittest.TestCase): @@ -99,24 +101,65 @@ class TestCustomer(unittest.TestCase): frappe.db.sql("delete from `tabCustomer` where customer_name='_Test Customer 1'") if not frappe.db.get_value("Customer", "_Test Customer 1"): - test_customer_1 = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test Customer 1", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert(ignore_permissions=True) + test_customer_1 = frappe.get_doc( + get_customer_dict('_Test Customer 1')).insert(ignore_permissions=True) else: test_customer_1 = frappe.get_doc("Customer", "_Test Customer 1") - duplicate_customer = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test Customer 1", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert(ignore_permissions=True) + duplicate_customer = frappe.get_doc( + get_customer_dict('_Test Customer 1')).insert(ignore_permissions=True) self.assertEquals("_Test Customer 1", test_customer_1.name) self.assertEquals("_Test Customer 1 - 1", duplicate_customer.name) self.assertEquals(test_customer_1.customer_name, duplicate_customer.customer_name) + + def test_party_status_open(self): + from erpnext.selling.doctype.quotation.test_quotation import get_quotation_dict + + customer = frappe.get_doc(get_customer_dict('Party Status Test')).insert() + self.assertEquals(frappe.db.get_value('Customer', customer.name, 'status'), 'Active') + + quotation = frappe.get_doc(get_quotation_dict(customer=customer.name)).insert() + self.assertEquals(frappe.db.get_value('Customer', customer.name, 'status'), 'Open') + + quotation.submit() + self.assertEquals(frappe.db.get_value('Customer', customer.name, 'status'), 'Active') + + quotation.cancel() + quotation.delete() + customer.delete() + + def test_party_status_close(self): + from erpnext.selling.doctype.quotation.test_quotation import get_quotation_dict + + customer = frappe.get_doc(get_customer_dict('Party Status Test')).insert() + self.assertEquals(frappe.db.get_value('Customer', customer.name, 'status'), 'Active') + + # open quotation + quotation = frappe.get_doc(get_quotation_dict(customer=customer.name)).insert() + self.assertEquals(frappe.db.get_value('Customer', customer.name, 'status'), 'Open') + + # close quotation (submit) + quotation.submit() + + quotation1 = frappe.get_doc(get_quotation_dict(customer=customer.name)).insert() + + # still open + self.assertEquals(frappe.db.get_value('Customer', customer.name, 'status'), 'Open') + + quotation.cancel() + quotation.delete() + + quotation1.delete() + + customer.delete() + +def get_customer_dict(customer_name): + return { + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory" + } + diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index e3b359daf6..36cc472574 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -69,3 +69,21 @@ class TestQuotation(unittest.TestCase): si.save() test_records = frappe.get_test_records('Quotation') + +def get_quotation_dict(customer=None, item_code=None): + if not customer: + customer = '_Test Customer' + if not item_code: + item_code = '_Test Item' + + return { + 'doctype': 'Quotation', + 'customer': customer, + 'items': [ + { + 'item_code': item_code, + 'qty': 1, + 'rate': 100 + } + ] + } \ No newline at end of file