From 15a7f215b95e04324f350605611968e61d8b5ca2 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 14 Apr 2016 17:30:40 +0530 Subject: [PATCH] [enhancement] heatmaps on item and notifications for item --- erpnext/accounts/party.py | 7 ++++ erpnext/buying/doctype/supplier/supplier.py | 13 +++++++- erpnext/hr/doctype/employee/employee.py | 22 +++++++++++++ erpnext/patches.txt | 1 + erpnext/patches/v7_0/update_item_projected.py | 7 ++++ erpnext/selling/doctype/customer/customer.py | 14 +++++++- erpnext/startup/notifications.py | 3 +- erpnext/stock/doctype/bin/bin.py | 7 ++++ erpnext/stock/doctype/item/item.js | 6 ++++ erpnext/stock/doctype/item/item.json | 27 +++++++++++++++- erpnext/stock/doctype/item/item.py | 32 ++++++++++++++++--- .../stock/doctype/item/item_dashboard.html | 12 +++++++ erpnext/stock/doctype/item/item_links.py | 3 +- erpnext/stock/doctype/item/item_list.js | 8 ++--- 14 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 erpnext/patches/v7_0/update_item_projected.py create mode 100644 erpnext/stock/doctype/item/item_dashboard.html diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e4cba75bae2..02d4244a555 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -324,3 +324,10 @@ def validate_party_frozen_disabled(party_type, party_name): frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier') if not frozen_accounts_modifier in frappe.get_roles(): frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen) + +def get_timeline_data(doctype, name): + '''returns timeline data for the past one year''' + from frappe.desk.form.load import get_communication_data + data = get_communication_data(doctype, name, fields = 'unix_timestamp(date(creation)), count(name)', + group_by='group by date(creation)', as_dict=False) + return dict(data) \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 4b384faed40..9a3d58c43ff 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -8,7 +8,7 @@ from frappe import msgprint, _ from frappe.model.naming import make_autoname from erpnext.utilities.address_and_contact import load_address_and_contact from erpnext.utilities.transaction_base import TransactionBase -from erpnext.accounts.party import validate_party_accounts +from erpnext.accounts.party import validate_party_accounts, get_timeline_data from erpnext.accounts.party_status import get_party_status class Supplier(TransactionBase): @@ -84,3 +84,14 @@ class Supplier(TransactionBase): frappe.db.sql("""update `tabAddress` set address_title=%(newdn)s {set_field} where supplier=%(newdn)s"""\ .format(set_field=set_field), ({"newdn": newdn})) + +@frappe.whitelist() +def get_dashboard_data(name): + '''load dashboard related data''' + frappe.has_permission(doc=frappe.get_doc('Supplier', name), throw=True) + + from frappe.desk.notifications import get_open_count + return { + 'count': get_open_count('Supplier', name), + 'timeline_data': get_timeline_data('Supplier', name), + } diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index e769e3e818b..9e02baf6ab7 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -157,6 +157,28 @@ class Employee(Document): def on_trash(self): delete_events(self.doctype, self.name) + def get_timeline_data(self): + '''returns timeline data based on attendance''' + return + +@frappe.whitelist() +def get_dashboard_data(name): + '''load dashboard related data''' + frappe.has_permission(doc=frappe.get_doc('Employee', name), throw=True) + + from frappe.desk.notifications import get_open_count + return { + 'count': get_open_count('Employee', name), + 'timeline_data': get_timeline_data(name), + } + +def get_timeline_data(name): + '''Return timeline for attendance''' + return dict(frappe.db.sql('''select unix_timestamp(att_date), count(*) + from `tabAttendance` where employee=%s + and att_date > date_sub(curdate(), interval 1 year) + and status in ('Present', 'Half Day') + group by att_date''', name)) @frappe.whitelist() def get_retirement_date(date_of_birth=None): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 975d58990bd..84a4e199b5f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -261,3 +261,4 @@ 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 +erpnext.patches.v7_0.update_item_projected diff --git a/erpnext/patches/v7_0/update_item_projected.py b/erpnext/patches/v7_0/update_item_projected.py new file mode 100644 index 00000000000..71b54af1420 --- /dev/null +++ b/erpnext/patches/v7_0/update_item_projected.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + frappe.reload_doctype("Item") + from erpnext.stock.doctype.bin.bin import update_item_projected_qty + for item in frappe.get_all("Item", filters={"is_stock_item": 1}): + update_item_projected_qty(item.name) \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 51aedef1aba..9902441df79 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -10,7 +10,7 @@ from frappe.utils import flt, cint, cstr from frappe.desk.reportview import build_match_conditions from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.address_and_contact import load_address_and_contact -from erpnext.accounts.party import validate_party_accounts +from erpnext.accounts.party import validate_party_accounts, get_timeline_data from erpnext.accounts.party_status import get_party_status class Customer(TransactionBase): @@ -128,6 +128,18 @@ class Customer(TransactionBase): {set_field} where customer=%(newdn)s"""\ .format(set_field=set_field), ({"newdn": newdn})) + +@frappe.whitelist() +def get_dashboard_data(name): + '''load dashboard related data''' + frappe.has_permission(doc=frappe.get_doc('Customer', name), throw=True) + + from frappe.desk.notifications import get_open_count + return { + 'count': get_open_count('Customer', name), + 'timeline_data': get_timeline_data('Customer', name), + } + def get_customer_list(doctype, txt, searchfield, start, page_len, filters): if frappe.db.get_default("cust_master_name") == "Customer Name": fields = ["name", "customer_group", "territory"] diff --git a/erpnext/startup/notifications.py b/erpnext/startup/notifications.py index 700ced20873..335efbb9127 100644 --- a/erpnext/startup/notifications.py +++ b/erpnext/startup/notifications.py @@ -10,6 +10,7 @@ def get_notification_config(): "Warranty Claim": {"status": "Open"}, "Task": {"status": "Overdue"}, "Project": {"status": "Open"}, + "Item": {"total_projected_qty": ("<", 0)}, "Customer": {"status": "Open"}, "Supplier": {"status": "Open"}, "Lead": {"status": "Open"}, @@ -40,7 +41,7 @@ def get_notification_config(): "docstatus": ("<", 2) }, "Purchase Receipt": {"docstatus": 0}, - "Production Order": { "status": "In Process" }, + "Production Order": { "status": ("in", ("Draft", "Not Started", "In Process")) }, "BOM": {"docstatus": 0}, "Timesheet": {"docstatus": 0}, "Time Log": {"status": "Draft"}, diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 168268653e2..3d57b4d9fc9 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -69,6 +69,7 @@ class Bin(Document): flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty) self.save() + update_item_projected_qty(self.item_code) def get_first_sle(self): sle = frappe.db.sql(""" @@ -79,3 +80,9 @@ class Bin(Document): limit 1 """, (self.item_code, self.warehouse), as_dict=1) return sle and sle[0] or None + +def update_item_projected_qty(item_code): + '''Set Item project qty''' + frappe.db.sql('''update tabItem set + total_projected_qty = (select sum(projected_qty) from tabBin where item_code=%s) + where name=%s''', (item_code, item_code)) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 8a9d83c9969..900c40c6896 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -16,6 +16,12 @@ frappe.ui.form.on("Item", { }, + dashboard_update: function(frm) { + if(frm.dashboard_data.stock_data && frm.dashboard_data.stock_data.length) { + frm.dashboard.add_stats(frappe.render_template('item_dashboard', {data: frm.dashboard_data.stock_data})) + } + }, + refresh: function(frm) { if(frm.doc.is_stock_item) { diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 94d2e8d6aff..43fccf614a8 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -2285,6 +2285,31 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "total_projected_qty", + "fieldtype": "Float", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Total Projected Qty", + "length": 0, + "no_copy": 0, + "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 } ], "hide_heading": 0, @@ -2298,7 +2323,7 @@ "issingle": 0, "istable": 0, "max_attachments": 1, - "modified": "2016-04-11 09:15:30.911215", + "modified": "2016-04-14 07:51:07.058298", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8286b5fbe9b..d43f68db3a1 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -407,13 +407,13 @@ class Item(WebsiteGenerator): if self.check_if_linked_document_exists(): frappe.throw(_("As there are existing transactions for this item, \ you can not change the values of 'Has Serial No', 'Has Batch No', 'Is Stock Item' and 'Valuation Method'")) - + def check_if_linked_document_exists(self): - for doctype in ("Sales Order Item", "Delivery Note Item", "Sales Invoice Item", - "Material Request Item", "Purchase Order Item", "Purchase Receipt Item", + for doctype in ("Sales Order Item", "Delivery Note Item", "Sales Invoice Item", + "Material Request Item", "Purchase Order Item", "Purchase Receipt Item", "Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"): if frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}) or \ - frappe.db.get_value("Production Order", + frappe.db.get_value("Production Order", filters={"production_item": self.name, "docstatus": 1}): return True @@ -583,6 +583,30 @@ class Item(WebsiteGenerator): if self.is_fixed_asset and self.is_stock_item: frappe.throw(_("Fixed Asset Item must be a non-stock item")) + +@frappe.whitelist() +def get_dashboard_data(name): + '''load dashboard related data''' + frappe.has_permission(doc=frappe.get_doc('Item', name), throw=True) + + from frappe.desk.notifications import get_open_count + return { + 'count': get_open_count('Item', name), + 'timeline_data': get_timeline_data(name), + 'stock_data': get_stock_data(name) + } + +def get_timeline_data(name): + '''returns timeline data based on stock ledger entry''' + return dict(frappe.db.sql('''select unix_timestamp(posting_date), count(*) + from `tabStock Ledger Entry` where item_code=%s + and posting_date > date_sub(curdate(), interval 1 year) + group by posting_date''', name)) + +def get_stock_data(name): + return frappe.get_all('Bin', fields=['warehouse', 'actual_qty', 'projected_qty'], + filters={'item_code': name}) + def validate_end_of_life(item_code, end_of_life=None, disabled=None, verbose=1): if (not end_of_life) or (disabled is None): end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) diff --git a/erpnext/stock/doctype/item/item_dashboard.html b/erpnext/stock/doctype/item/item_dashboard.html new file mode 100644 index 00000000000..a002a50fee6 --- /dev/null +++ b/erpnext/stock/doctype/item/item_dashboard.html @@ -0,0 +1,12 @@ +
+
Stock Levels
+
+
+
    + {% data.every(function(d) { %} +
  • {{ d.warehouse }}: {{ d.actual_qty }} ({{ d.projected_qty }})
  • + {% }) %} +
+
+
+
\ No newline at end of file diff --git a/erpnext/stock/doctype/item/item_links.py b/erpnext/stock/doctype/item/item_links.py index c1fff8c34ee..5586214e8f0 100644 --- a/erpnext/stock/doctype/item/item_links.py +++ b/erpnext/stock/doctype/item/item_links.py @@ -4,7 +4,8 @@ links = { 'fieldname': 'item_code', 'non_standard_fieldnames': { 'Production Order': 'production_item', - 'Product Bundle': 'new_item_code' + 'Product Bundle': 'new_item_code', + 'Batch': 'item' }, 'transactions': [ { diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js index fffc7fe5261..2fee589a3c1 100644 --- a/erpnext/stock/doctype/item/item_list.js +++ b/erpnext/stock/doctype/item/item_list.js @@ -1,10 +1,12 @@ frappe.listview_settings['Item'] = { add_fields: ["item_name", "stock_uom", "item_group", "image", "variant_of", - "has_variants", "end_of_life", "disabled", "is_sales_item"], + "has_variants", "end_of_life", "disabled", "total_projected_qty"], filters: [["disabled", "=", "0"]], get_indicator: function(doc) { - if (doc.disabled) { + if(doc.total_projected_qty < 0) { + return [__("Shortage"), "red", "total_projected_qty,<,0"]; + } else if (doc.disabled) { return [__("Disabled"), "grey", "disabled,=,Yes"]; } else if (doc.end_of_life && doc.end_of_life < frappe.datetime.get_today()) { return [__("Expired"), "grey", "end_of_life,<,Today"]; @@ -12,8 +14,6 @@ frappe.listview_settings['Item'] = { return [__("Template"), "blue", "has_variants,=,Yes"]; } else if (doc.variant_of) { return [__("Variant"), "green", "variant_of,=," + doc.variant_of]; - } else { - return [__("Active"), "blue", "end_of_life,>=,Today"]; } } };