From 6280609c25e7c93e47a20a1b21c57fc25fe79084 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 14 Jan 2020 15:11:35 +0530 Subject: [PATCH 1/7] enhancement: add territory wise sales report --- .../crm/doctype/opportunity/opportunity.json | 6 + .../report/territory_wise_sales/__init__.py | 0 .../territory_wise_sales.js | 16 ++ .../territory_wise_sales.json | 21 +++ .../territory_wise_sales.py | 137 ++++++++++++++++++ 5 files changed, 180 insertions(+) create mode 100644 erpnext/selling/report/territory_wise_sales/__init__.py create mode 100644 erpnext/selling/report/territory_wise_sales/territory_wise_sales.js create mode 100644 erpnext/selling/report/territory_wise_sales/territory_wise_sales.json create mode 100644 erpnext/selling/report/territory_wise_sales/territory_wise_sales.py diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 66e3ca48dd2..0e2068a0a57 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -22,6 +22,7 @@ "sales_stage", "order_lost_reason", "mins_to_first_response", + "expected_closing", "next_contact", "contact_by", "contact_date", @@ -156,6 +157,11 @@ "label": "Mins to first response", "read_only": 1 }, + { + "fieldname": "expected_closing", + "fieldtype": "Date", + "label": "Expected Closing Date" + }, { "collapsible": 1, "collapsible_depends_on": "contact_by", diff --git a/erpnext/selling/report/territory_wise_sales/__init__.py b/erpnext/selling/report/territory_wise_sales/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js new file mode 100644 index 00000000000..12f53048134 --- /dev/null +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + + +frappe.query_reports["Territory wise Sales"] = { + "breadcrumb":"Selling", + "filters": [ + { + fieldname:"expected_closing_date", + label: __("Expected Closing Date"), + fieldtype: "DateRange", + default: [frappe.datetime.add_months(frappe.datetime.get_today(),-1), frappe.datetime.get_today()] + } + ] +}; diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.json b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.json new file mode 100644 index 00000000000..88dfe8a1298 --- /dev/null +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.json @@ -0,0 +1,21 @@ +{ + "add_total_row": 0, + "creation": "2020-01-10 13:02:23.312515", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-01-14 14:50:33.863423", + "modified_by": "Administrator", + "module": "Selling", + "name": "Territory wise Sales", + "owner": "Administrator", + "prepared_report": 0, + "query": "", + "ref_doctype": "Opportunity", + "report_name": "Territory wise Sales", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py new file mode 100644 index 00000000000..12582a6e95a --- /dev/null +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -0,0 +1,137 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from erpnext import get_default_currency +from frappe import _ + +def execute(filters=None): + filters = frappe._dict(filters) + columns = get_columns() + data = get_data(filters) + return columns, data + + +def get_columns(): + currency = get_default_currency() + return [ + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory" + }, + { + "label": _("Opportunity Amount"), + "fieldname": "opportunity_amount", + "fieldtype": "Currency", + "options": currency + }, + { + "label": _("Quotation Amount"), + "fieldname": "quotation_amount", + "fieldtype": "Currency", + "options": currency + }, + { + "label": _("Order Amount"), + "fieldname": "order_amount", + "fieldtype": "Currency", + "options": currency + }, + { + "label": _("Billing Amount"), + "fieldname": "billing_amount", + "fieldtype": "Currency", + "options": currency + } + ] + +def get_data(filters=None): + data = [] + + opportunities = get_opportunities(filters) + quotations = get_quotations(opportunities) + sales_orders = get_sales_orders(quotations) + sales_invoices = get_sales_invoice(sales_orders) + + for territory in frappe.get_all("Territory"): + territory_opportunities = list(filter(lambda x: x.territory == territory.name, opportunities)) if opportunities and opportunities else None + t_opportunity_names = [t.name for t in territory_opportunities] if territory_opportunities else None + + territory_quotations = list(filter(lambda x: x.opportunity in t_opportunity_names, quotations)) if t_opportunity_names and quotations else None + t_quotation_names = [t.name for t in territory_quotations] if territory_quotations else None + + territory_orders = list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) if t_quotation_names and sales_orders else None + t_order_names = [t.name for t in territory_orders] if territory_orders else None + + territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else None + + territory_data = { + "territory": territory.name, + "opportunity_amount": _get_total(territory_opportunities, "opportunity_amount"), + "quotation_amount": _get_total(territory_quotations), + "order_amount": _get_total(territory_orders), + "billing_amount": _get_total(territory_invoices) + } + data.append(territory_data) + + return data + +def get_opportunities(filters): + conditions = "" + + if filters.from_date and filters.to_date: + conditions = " WHERE expected_closing between %(from_date)s and %(to_date)s" + + return frappe.db.sql(""" + SELECT name, territory, opportunity_amount + FROM `tabOpportunity` {0} + """.format(conditions), filters, as_dict=1) + +def get_quotations(opportunities): + if not opportunities: + return [] + + opportunity_names = [o.name for o in opportunities] + + return frappe.db.sql(""" + SELECT `name`,`base_grand_total`, `opportunity` + FROM `tabQuotation` + WHERE docstatus=1 AND opportunity in ({0}) + """.format(', '.join(["%s"]*len(opportunity_names))), tuple(opportunity_names), as_dict=1) + +def get_sales_orders(quotations): + if not quotations: + return [] + + quotation_names = [q.name for q in quotations] + + return frappe.db.sql(""" + SELECT so.`name`, so.`base_grand_total`, soi.prevdoc_docname as quotation + FROM `tabSales Order` so, `tabSales Order Item` soi + WHERE so.docstatus=1 AND so.name = soi.parent AND soi.prevdoc_docname in ({0}) + """.format(', '.join(["%s"]*len(quotation_names))), tuple(quotation_names), as_dict=1) + +def get_sales_invoice(sales_orders): + if not sales_orders: + return [] + + so_names = [so.name for so in sales_orders] + + return frappe.db.sql(""" + SELECT si.name, si.base_grand_total, sii.sales_order + FROM `tabSales Invoice` si, `tabSales Invoice Item` sii + WHERE si.docstatus=1 AND si.name = sii.parent AND sii.sales_order in ({0}) + """.format(', '.join(["%s"]*len(so_names))), tuple(so_names), as_dict=1) + +def _get_total(doclist, amount_field="base_grand_total"): + if not doclist: + return 0 + + total = 0 + for doc in doclist: + total = total + doc.get(amount_field, 0) + + return total From 75adb808e5dc4baf5c704be1facf5c2fad87c9c8 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 31 Jan 2020 10:35:47 +0530 Subject: [PATCH 2/7] various fixes from review - Rename the report to title case - Added company filter - fixed checks in filter and += operator for total --- .../territory_wise_sales.js | 8 +++++- .../territory_wise_sales.json | 18 ++++++++----- .../territory_wise_sales.py | 26 ++++++++++++------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js index 12f53048134..767d5290ebe 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js @@ -10,7 +10,13 @@ frappe.query_reports["Territory wise Sales"] = { fieldname:"expected_closing_date", label: __("Expected Closing Date"), fieldtype: "DateRange", - default: [frappe.datetime.add_months(frappe.datetime.get_today(),-1), frappe.datetime.get_today()] + default: [frappe.datetime.add_months(frappe.datetime.get_today(),-1), frappe.datetime.get_today()], + }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", } ] }; diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.json b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.json index 88dfe8a1298..b98d1a2f329 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.json +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.json @@ -1,21 +1,27 @@ { "add_total_row": 0, - "creation": "2020-01-10 13:02:23.312515", + "creation": "2020-01-31 10:34:33.319047", "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2020-01-14 14:50:33.863423", + "modified": "2020-01-31 10:34:33.319047", "modified_by": "Administrator", "module": "Selling", - "name": "Territory wise Sales", + "name": "Territory-wise Sales", "owner": "Administrator", "prepared_report": 0, - "query": "", "ref_doctype": "Opportunity", - "report_name": "Territory wise Sales", + "report_name": "Territory-wise Sales", "report_type": "Script Report", - "roles": [] + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + } + ] } \ No newline at end of file diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index 12582a6e95a..c8a63ee29f1 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -57,16 +57,16 @@ def get_data(filters=None): sales_invoices = get_sales_invoice(sales_orders) for territory in frappe.get_all("Territory"): - territory_opportunities = list(filter(lambda x: x.territory == territory.name, opportunities)) if opportunities and opportunities else None + territory_opportunities = list(filter(lambda x: x.territory == territory.name, opportunities)) if opportunities and opportunities else [] t_opportunity_names = [t.name for t in territory_opportunities] if territory_opportunities else None - territory_quotations = list(filter(lambda x: x.opportunity in t_opportunity_names, quotations)) if t_opportunity_names and quotations else None + territory_quotations = list(filter(lambda x: x.opportunity in t_opportunity_names, quotations)) if t_opportunity_names and quotations else [] t_quotation_names = [t.name for t in territory_quotations] if territory_quotations else None - territory_orders = list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) if t_quotation_names and sales_orders else None + territory_orders = list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) if t_quotation_names and sales_orders else [] t_order_names = [t.name for t in territory_orders] if territory_orders else None - territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else None + territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else [] territory_data = { "territory": territory.name, @@ -84,11 +84,19 @@ def get_opportunities(filters): if filters.from_date and filters.to_date: conditions = " WHERE expected_closing between %(from_date)s and %(to_date)s" + + if filters.company: + if conditions: + conditions += " AND" + else: + conditions += " WHERE" + conditions += " company = %(company)s" + return frappe.db.sql(""" SELECT name, territory, opportunity_amount FROM `tabOpportunity` {0} - """.format(conditions), filters, as_dict=1) + """.format(conditions), filters, as_dict=1) #nosec def get_quotations(opportunities): if not opportunities: @@ -100,7 +108,7 @@ def get_quotations(opportunities): SELECT `name`,`base_grand_total`, `opportunity` FROM `tabQuotation` WHERE docstatus=1 AND opportunity in ({0}) - """.format(', '.join(["%s"]*len(opportunity_names))), tuple(opportunity_names), as_dict=1) + """.format(', '.join(["%s"]*len(opportunity_names))), tuple(opportunity_names), as_dict=1) #nosec def get_sales_orders(quotations): if not quotations: @@ -112,7 +120,7 @@ def get_sales_orders(quotations): SELECT so.`name`, so.`base_grand_total`, soi.prevdoc_docname as quotation FROM `tabSales Order` so, `tabSales Order Item` soi WHERE so.docstatus=1 AND so.name = soi.parent AND soi.prevdoc_docname in ({0}) - """.format(', '.join(["%s"]*len(quotation_names))), tuple(quotation_names), as_dict=1) + """.format(', '.join(["%s"]*len(quotation_names))), tuple(quotation_names), as_dict=1) #nosec def get_sales_invoice(sales_orders): if not sales_orders: @@ -124,7 +132,7 @@ def get_sales_invoice(sales_orders): SELECT si.name, si.base_grand_total, sii.sales_order FROM `tabSales Invoice` si, `tabSales Invoice Item` sii WHERE si.docstatus=1 AND si.name = sii.parent AND sii.sales_order in ({0}) - """.format(', '.join(["%s"]*len(so_names))), tuple(so_names), as_dict=1) + """.format(', '.join(["%s"]*len(so_names))), tuple(so_names), as_dict=1) #nosec def _get_total(doclist, amount_field="base_grand_total"): if not doclist: @@ -132,6 +140,6 @@ def _get_total(doclist, amount_field="base_grand_total"): total = 0 for doc in doclist: - total = total + doc.get(amount_field, 0) + total += doc.get(amount_field, 0) return total From b1473c3de0577f00dff9749192a8717a41aefe81 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 31 Jan 2020 10:43:00 +0530 Subject: [PATCH 3/7] fix: return empty array --- .../report/territory_wise_sales/territory_wise_sales.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index c8a63ee29f1..842e6345106 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -58,13 +58,13 @@ def get_data(filters=None): for territory in frappe.get_all("Territory"): territory_opportunities = list(filter(lambda x: x.territory == territory.name, opportunities)) if opportunities and opportunities else [] - t_opportunity_names = [t.name for t in territory_opportunities] if territory_opportunities else None + t_opportunity_names = [t.name for t in territory_opportunities] if territory_opportunities else [] territory_quotations = list(filter(lambda x: x.opportunity in t_opportunity_names, quotations)) if t_opportunity_names and quotations else [] - t_quotation_names = [t.name for t in territory_quotations] if territory_quotations else None + t_quotation_names = [t.name for t in territory_quotations] if territory_quotations else [] territory_orders = list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) if t_quotation_names and sales_orders else [] - t_order_names = [t.name for t in territory_orders] if territory_orders else None + t_order_names = [t.name for t in territory_orders] if territory_orders else [] territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else [] From 9c4399e1f2b5fe086a0445ffb08c50e6666fa2a1 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 3 Feb 2020 14:57:42 +0530 Subject: [PATCH 4/7] fix: better syntax for checking empty arrays --- .../territory_wise_sales.js | 4 +-- .../territory_wise_sales.py | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js index 767d5290ebe..ca78be45a69 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js @@ -7,8 +7,8 @@ frappe.query_reports["Territory wise Sales"] = { "breadcrumb":"Selling", "filters": [ { - fieldname:"expected_closing_date", - label: __("Expected Closing Date"), + fieldname:"transaction_date", + label: __("Transaction Date"), fieldtype: "DateRange", default: [frappe.datetime.add_months(frappe.datetime.get_today(),-1), frappe.datetime.get_today()], }, diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index 842e6345106..415d078b718 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -57,14 +57,26 @@ def get_data(filters=None): sales_invoices = get_sales_invoice(sales_orders) for territory in frappe.get_all("Territory"): - territory_opportunities = list(filter(lambda x: x.territory == territory.name, opportunities)) if opportunities and opportunities else [] - t_opportunity_names = [t.name for t in territory_opportunities] if territory_opportunities else [] + territory_opportunities = [] + if opportunities: + territory_opportunities = list(filter(lambda x: x.territory == territory.name, opportunities)) + t_opportunity_names = [] + if territory_opportunities: + t_opportunity_names = [t.name for t in territory_opportunities] - territory_quotations = list(filter(lambda x: x.opportunity in t_opportunity_names, quotations)) if t_opportunity_names and quotations else [] - t_quotation_names = [t.name for t in territory_quotations] if territory_quotations else [] + territory_quotations = [] + if t_opportunity_names and quotations: + territory_quotations = list(filter(lambda x: x.opportunity in t_opportunity_names, quotations)) + t_quotation_names = [] + if territory_quotations: + t_quotation_names = [t.name for t in territory_quotations] - territory_orders = list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) if t_quotation_names and sales_orders else [] - t_order_names = [t.name for t in territory_orders] if territory_orders else [] + territory_orders = [] + if t_quotation_names and sales_orders: + list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) + t_order_names = [] + if territory_orders: + t_order_names = [t.name for t in territory_orders] territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else [] @@ -83,7 +95,7 @@ def get_opportunities(filters): conditions = "" if filters.from_date and filters.to_date: - conditions = " WHERE expected_closing between %(from_date)s and %(to_date)s" + conditions = " WHERE transaction_date between %(from_date)s and %(to_date)s" if filters.company: if conditions: From 9d9a3d85d86369b3381613d5766cd8a282ad13a2 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 5 Feb 2020 14:17:21 +0530 Subject: [PATCH 5/7] fix: filters after rename --- .../selling/report/territory_wise_sales/territory_wise_sales.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js index ca78be45a69..bef800f1040 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.js @@ -3,7 +3,7 @@ /* eslint-disable */ -frappe.query_reports["Territory wise Sales"] = { +frappe.query_reports["Territory-wise Sales"] = { "breadcrumb":"Selling", "filters": [ { From a4928f6f5b9845a3db92dd7db7081f30775845fd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 6 Feb 2020 12:50:25 +0530 Subject: [PATCH 6/7] fix: Add report link in module view and fix date filter --- erpnext/config/crm.py | 7 +++++++ .../report/territory_wise_sales/territory_wise_sales.py | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/config/crm.py b/erpnext/config/crm.py index cf1021948a5..09c2a65633b 100644 --- a/erpnext/config/crm.py +++ b/erpnext/config/crm.py @@ -117,6 +117,13 @@ def get_data(): "name": "Lead Owner Efficiency", "doctype": "Lead", "dependencies": ["Lead"] + }, + { + "type": "report", + "is_query_report": True, + "name": "Territory-wise Sales", + "doctype": "Opportunity", + "dependencies": ["Opportunity"] } ] }, diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index 415d078b718..656ff33ab61 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -94,8 +94,10 @@ def get_data(filters=None): def get_opportunities(filters): conditions = "" - if filters.from_date and filters.to_date: - conditions = " WHERE transaction_date between %(from_date)s and %(to_date)s" + if filters.get('transaction_date'): + conditions = " WHERE transaction_date between {0} and {1}".format( + frappe.db.escape(filters['transaction_date'][0]), + frappe.db.escape(filters['transaction_date'][1])) if filters.company: if conditions: @@ -108,7 +110,7 @@ def get_opportunities(filters): return frappe.db.sql(""" SELECT name, territory, opportunity_amount FROM `tabOpportunity` {0} - """.format(conditions), filters, as_dict=1) #nosec + """.format(conditions), filters, as_dict=1, debug=1) #nosec def get_quotations(opportunities): if not opportunities: From da406d74ef8fd9024c5c76853d0b331de7801884 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 6 Feb 2020 12:55:21 +0530 Subject: [PATCH 7/7] fix: Remove debug param --- .../selling/report/territory_wise_sales/territory_wise_sales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index 656ff33ab61..f2db478686f 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -110,7 +110,7 @@ def get_opportunities(filters): return frappe.db.sql(""" SELECT name, territory, opportunity_amount FROM `tabOpportunity` {0} - """.format(conditions), filters, as_dict=1, debug=1) #nosec + """.format(conditions), filters, as_dict=1) #nosec def get_quotations(opportunities): if not opportunities: