From df547d3247edeef0557635710b71fdda51fc76d8 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Mon, 25 Mar 2019 15:34:55 +0500 Subject: [PATCH 01/13] feat(Sales/Purchase Details Report): Party-Transaction-Item tree view --- .../report/purchase_details/__init__.py | 0 .../purchase_details/purchase_details.js | 76 +++ .../purchase_details/purchase_details.json | 28 + .../purchase_details/purchase_details.py | 8 + .../selling/report/sales_details/__init__.py | 0 .../report/sales_details/sales_details.js | 90 ++++ .../report/sales_details/sales_details.json | 28 + .../report/sales_details/sales_details.py | 501 ++++++++++++++++++ 8 files changed, 731 insertions(+) create mode 100644 erpnext/buying/report/purchase_details/__init__.py create mode 100644 erpnext/buying/report/purchase_details/purchase_details.js create mode 100644 erpnext/buying/report/purchase_details/purchase_details.json create mode 100644 erpnext/buying/report/purchase_details/purchase_details.py create mode 100644 erpnext/selling/report/sales_details/__init__.py create mode 100644 erpnext/selling/report/sales_details/sales_details.js create mode 100644 erpnext/selling/report/sales_details/sales_details.json create mode 100644 erpnext/selling/report/sales_details/sales_details.py diff --git a/erpnext/buying/report/purchase_details/__init__.py b/erpnext/buying/report/purchase_details/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/buying/report/purchase_details/purchase_details.js b/erpnext/buying/report/purchase_details/purchase_details.js new file mode 100644 index 000000000000..40a4ff5b5ee6 --- /dev/null +++ b/erpnext/buying/report/purchase_details/purchase_details.js @@ -0,0 +1,76 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Purchase Details"] = { + "filters": [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_end_date"), + reqd: 1 + }, + { + fieldname: "doctype", + label: __("Based On"), + fieldtype: "Select", + options: ["Purchase Order","Purchase Receipt","Purchase Invoice"], + default: "Purchase Invoice", + reqd: 1 + }, + { + fieldname: "qty_field", + label: __("Quantity Type"), + fieldtype: "Select", + options: ["Stock Qty", "Contents Qty", "Transaction Qty"], + default: "Stock Qty", + reqd: 1 + }, + { + fieldname: "supplier", + label: __("Supplier"), + fieldtype: "Link", + options: "Supplier" + }, + { + fieldname: "supplier_group", + label: __("Supplier Group"), + fieldtype: "Link", + options: "Supplier Group" + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item" + }, + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group" + }, + { + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand" + } + ], +} diff --git a/erpnext/buying/report/purchase_details/purchase_details.json b/erpnext/buying/report/purchase_details/purchase_details.json new file mode 100644 index 000000000000..656cb6c8203b --- /dev/null +++ b/erpnext/buying/report/purchase_details/purchase_details.json @@ -0,0 +1,28 @@ +{ + "add_total_row": 0, + "creation": "2019-03-25 15:25:54.566163", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Techno Automotive Refinish", + "modified": "2019-03-25 15:25:54.566163", + "modified_by": "Administrator", + "module": "Buying", + "name": "Purchase Details", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Purchase Details", + "report_type": "Script Report", + "roles": [ + { + "role": "Purchase User" + }, + { + "role": "Accounts User" + } + ] +} \ No newline at end of file diff --git a/erpnext/buying/report/purchase_details/purchase_details.py b/erpnext/buying/report/purchase_details/purchase_details.py new file mode 100644 index 000000000000..4079050cdb1c --- /dev/null +++ b/erpnext/buying/report/purchase_details/purchase_details.py @@ -0,0 +1,8 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from erpnext.selling.report.sales_details.sales_details import SalesPurchaseDetailsReport + +def execute(filters=None): + return SalesPurchaseDetailsReport(filters).run("Supplier") diff --git a/erpnext/selling/report/sales_details/__init__.py b/erpnext/selling/report/sales_details/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/selling/report/sales_details/sales_details.js b/erpnext/selling/report/sales_details/sales_details.js new file mode 100644 index 000000000000..c72986922726 --- /dev/null +++ b/erpnext/selling/report/sales_details/sales_details.js @@ -0,0 +1,90 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Sales Details"] = { + "filters": [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + }, + { + fieldname: "doctype", + label: __("Based On"), + fieldtype: "Select", + options: ["Sales Order","Delivery Note","Sales Invoice"], + default: "Sales Invoice", + reqd: 1 + }, + { + fieldname: "qty_field", + label: __("Quantity Type"), + fieldtype: "Select", + options: ["Stock Qty", "Contents Qty", "Transaction Qty"], + default: "Stock Qty", + reqd: 1 + }, + { + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer" + }, + { + fieldname: "customer_group", + label: __("Customer Group"), + fieldtype: "Link", + options: "Customer Group" + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item" + }, + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group" + }, + { + fieldname: "brand", + label: __("Brand"), + fieldtype: "Link", + options: "Brand" + }, + { + fieldname: "territory", + label: __("Territory"), + fieldtype: "Link", + options: "Territory" + }, + { + fieldname: "sales_person", + label: __("Sales Person"), + fieldtype: "Link", + options: "Sales Person" + } + ], +} + + diff --git a/erpnext/selling/report/sales_details/sales_details.json b/erpnext/selling/report/sales_details/sales_details.json new file mode 100644 index 000000000000..0c80ac48d45b --- /dev/null +++ b/erpnext/selling/report/sales_details/sales_details.json @@ -0,0 +1,28 @@ +{ + "add_total_row": 0, + "creation": "2019-03-24 16:27:15.822005", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Techno Automotive Refinish", + "modified": "2019-03-24 16:27:15.822005", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Details", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Sales Details", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Sales User" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py new file mode 100644 index 000000000000..77d4f38ef4d6 --- /dev/null +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -0,0 +1,501 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _, scrub +from frappe.utils import getdate, nowdate, flt +from collections import OrderedDict +from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import get_tax_accounts +from six import iteritems + +class SalesPurchaseDetailsReport(object): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.filters.from_date = getdate(self.filters.from_date or nowdate()) + self.filters.to_date = getdate(self.filters.to_date or nowdate()) + + self.additional_customer_info = frappe._dict() + + self.date_field = 'transaction_date' \ + if self.filters.doctype in ['Sales Order', 'Purchase Order'] else 'posting_date' + + if not self.filters.get("company"): + self.filters["company"] = frappe.db.get_single_value('Global Defaults', 'default_company') + self.company_currency = frappe.get_cached_value('Company', self.filters.get("company"), "default_currency") + + def run(self, party_type): + if self.filters.from_date > self.filters.to_date: + frappe.throw(_("From Date must be before To Date")) + + self.filters.party_type = party_type + + data = self.get_data() + columns = self.get_columns() + return columns, data + + def get_columns(self): + show_name = False + if self.filters.tree_type == "Customer": + if frappe.defaults.get_global_default('cust_master_name') == "Naming Series": + show_name = True + if self.filters.tree_type == "Supplier": + if frappe.defaults.get_global_default('supp_master_name') == "Naming Series": + show_name = True + if frappe.defaults.get_global_default('item_naming_by') == "Naming Series": + show_name = True + + columns = [ + { + "label": _("Reference"), + "fieldtype": "Dynamic Link", + "fieldname": "docname", + "options": "doctype", + "width": 400 + } + ] + + if show_name: + columns.append({ + "label": _("Name"), + "fieldtype": "Data", + "fieldname": "name", + "width": 150 + }) + + columns += [ + { + "label": _("Type"), + "fieldtype": "Data", + "fieldname": "doctype", + "width": 110 + }, + { + "label": _("Date"), + "fieldtype": "Date", + "fieldname": "date", + "width": 80 + }, + { + "label": _("UOM"), + "fieldtype": "Link", + "options": "UOM", + "fieldname": "uom", + "width": 50 + }, + { + "label": _("Qty"), + "fieldtype": "Float", + "fieldname": "qty", + "width": 90 + }, + { + "label": _("Net Rate"), + "fieldtype": "Currency", + "fieldname": "base_net_rate", + "options": "currency", + "width": 120 + }, + { + "label": _("Net Amount"), + "fieldtype": "Currency", + "fieldname": "base_net_amount", + "options": "currency", + "width": 120 + }, + { + "label": _("Rate"), + "fieldtype": "Currency", + "fieldname": "base_rate", + "options": "currency", + "width": 120 + }, + { + "label": _("Amount"), + "fieldtype": "Currency", + "fieldname": "base_amount", + "options": "currency", + "width": 120 + }, + { + "label": _("Total Tax Amount"), + "fieldtype": "Currency", + "fieldname": "tax_total_amount", + "options": "currency", + "width": 120 + }, + ] + + for tax_description in self.tax_columns: + amount_field = "tax_" + scrub(tax_description) + rate_field = amount_field + "_rate" + columns += [ + { + "label": _(tax_description) + " (%)", + "fieldtype": "Percent", + "fieldname": rate_field, + "width": 60 + }, + { + "label": _(tax_description), + "fieldtype": "Currency", + "fieldname": amount_field, + "options": "currency", + "width": 120 + }, + ] + + if self.filters.party_type == "Customer": + columns += [ + { + "label": _("Sales Person"), + "fieldtype": "Data", + "fieldname": "sales_person", + "width": 150 + }, + { + "label": _("Territory"), + "fieldtype": "Link", + "fieldname": "territory", + "options": "Territory", + "width": 100 + }, + ] + + columns += [ + { + "label": _("Group"), + "fieldtype": "Dynamic Link", + "fieldname": "group", + "options": "group_doctype", + "width": 100 + }, + { + "label": _("Brand"), + "fieldtype": "Link", + "fieldname": "brand", + "options": "Brand", + "width": 100 + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "width": 50 + }, + ] + + return columns + + def get_data(self): + self.get_entries() + self.build_tree() + + data = [] + self.total_row["indent"] = 0 + self.total_row["_collapsed"] = True + set_row_average_fields(self.total_row, self.tax_columns) + data.append(self.total_row) + + for party, docs in iteritems(self.tree): + party_row = self.party_totals[party] + set_row_average_fields(party_row, self.tax_columns) + party_row["indent"] = 1 + data.append(party_row) + + for docname, items_uoms in iteritems(docs): + doc_row = self.doc_totals[docname] + set_row_average_fields(doc_row, self.tax_columns) + doc_row["indent"] = 2 + data.append(doc_row) + + for item_code, uom in items_uoms: + item_row = self.doc_item_uom_totals[(docname, item_code, uom)] + set_row_average_fields(item_row, self.tax_columns) + item_row["indent"] = 3 + data.append(item_row) + + return data + + def build_tree(self): + # Totals Row Template + total_fields = ['qty', 'base_net_amount', 'base_amount'] + tax_amount_fields = ["tax_" + scrub(tax) for tax in self.tax_columns] + tax_rate_fields = ["tax_" + scrub(tax) + "_rate" for tax in self.tax_columns] + totals_template = {"currency": self.company_currency, "tax_total_amount": 0.0} + for f in total_fields: + totals_template[f] = 0.0 + for f in tax_amount_fields + tax_rate_fields: + totals_template[f] = 0.0 + for f in tax_rate_fields: + totals_template[f+"_count"] = 0 + + # Containers + self.tree = OrderedDict() + self.party_totals = {} + self.doc_totals = {} + self.doc_item_uom_totals = {} + self.total_row = {"docname": _("'Total'")} + self.total_row.update(totals_template) + + # Build tree and group totals + for d in self.entries: + # Set UOM based on qty field + if self.filters.qty_field == "Transaction Qty": + d.uom = d.uom + elif self.filters.qty_field == "Contents Qty": + d.uom = d.alt_uom or d.stock_uom + else: + d.uom = d.stock_uom + + # Add tree nodes if not already there + self.tree.setdefault(d.party, OrderedDict())\ + .setdefault(d.parent, set())\ + .add((d.item_code, d.uom)) + + # Party total row + if d.party not in self.party_totals: + party_row = self.party_totals[d.party] = totals_template.copy() + party_row.update({ + "doctype": self.filters.party_type, + "docname": d.party, + "name": d.party_name, + "group": d.party_group, + "group_doctype": d.party_group_dt + }) + if self.filters.party_type == "Customer": + details = self.additional_customer_info.get(d.party, frappe._dict()) + party_row.update({ + "sales_person": details.sales_person, + "territory": details.territory + }) + else: + party_row = self.party_totals[d.party] + + # Document total row + if d.parent not in self.doc_totals: + doc_row = self.doc_totals[d.parent] = totals_template.copy() + doc_row.update({ + "date": d.date, + "doctype": self.filters.doctype, + "docname": d.parent + }) + if self.filters.party_type == "Customer": + doc_row.update({ + "sales_person": d.sales_person, + "territory": d.territory + }) + else: + doc_row = self.doc_totals[d.parent] + + # Doc-Item-UOM row + if (d.parent, d.item_code, d.uom) not in self.doc_item_uom_totals: + item_row = self.doc_item_uom_totals[(d.parent, d.item_code, d.uom)] = totals_template.copy() + item_row.update({ + "date": d.date, + "doctype": "Item", + "docname": d.item_code, + "name": d.item_name, + "uom": d.uom, + "group": d.item_group, + "group_doctype": "Item Group", + "brand": d.brand + }) + if self.filters.party_type == "Customer": + item_row.update({ + "sales_person": d.sales_person, + "territory": d.territory + }) + else: + item_row = self.doc_item_uom_totals[(d.parent, d.item_code, d.uom)] + + for f in total_fields: + party_row[f] += d[f] + doc_row[f] += d[f] + item_row[f] += d[f] + self.total_row[f] += d[f] + + for f, tax in zip(tax_amount_fields, self.tax_columns): + tax_amount = self.itemsed_tax.get(d.name, {}).get(tax, {}).get("tax_amount", 0.0) + party_row[f] += tax_amount + party_row["tax_total_amount"] += tax_amount + doc_row[f] += tax_amount + doc_row["tax_total_amount"] += tax_amount + item_row[f] += tax_amount + item_row["tax_total_amount"] += tax_amount + self.total_row[f] += tax_amount + self.total_row["tax_total_amount"] += tax_amount + for f, tax in zip(tax_rate_fields, self.tax_columns): + tax_rate = self.itemsed_tax.get(d.name, {}).get(tax, {}).get("tax_rate", 0.0) + if tax_rate: + party_row[f] += tax_rate + party_row[f+"_count"] += 1 + doc_row[f] += tax_rate + doc_row[f+"_count"] += 1 + item_row[f] += tax_rate + item_row[f+"_count"] += 1 + self.total_row[f] += tax_rate + self.total_row[f+"_count"] += 1 + + def get_entries(self): + party_field = scrub(self.filters.party_type) + party_name_field = party_field + "_name" + qty_field = self.get_qty_fieldname() + + sales_person_table = ", `tabSales Team` sp" if self.filters.party_type == "Customer" else "" + sales_person_condition = "and sp.parent = s.name and sp.parenttype = %(doctype)s" if self.filters.party_type == "Customer" else "" + sales_person_field = ", GROUP_CONCAT(DISTINCT sp.sales_person SEPARATOR ', ') as sales_person" if self.filters.party_type == "Customer" else "" + + supplier_table = ", `tabSupplier` sup" if self.filters.party_type == "Supplier" else "" + supplier_condition = "and sup.name = s.supplier" if self.filters.party_type == "Supplier" else "" + + territory_field = ", s.territory" if self.filters.party_type == "Customer" else "" + + party_group_field = ", s.customer_group as party_group, 'Customer Group' as party_group_dt" if self.filters.party_type == "Customer"\ + else ", sup.supplier_group as party_group, 'Supplier Group' as party_group_dt" + + is_opening_condition = "and s.is_opening != 'Yes'" if self.filters.doctype in ['Sales Invoice', 'Purchase Invoice']\ + else "" + + filter_conditions = self.get_conditions() + + self.entries = frappe.db.sql(""" + select + s.name as parent, i.name, s.{date_field} as date, + s.{party_field} as party, s.{party_name_field} as party_name, + i.item_code, i.item_name, + i.{qty_field} as qty, + i.uom, i.stock_uom, i.alt_uom, + i.base_net_amount, i.base_amount, + i.brand, i.item_group + {party_group_field} {sales_person_field} {territory_field} + from + `tab{doctype} Item` i, `tab{doctype}` s {sales_person_table} {supplier_table} + where i.parent = s.name and s.docstatus = 1 and s.company = %(company)s + and s.{date_field} between %(from_date)s and %(to_date)s + {sales_person_condition} {supplier_condition} {is_opening_condition} {filter_conditions} + group by s.name, i.name + order by s.{date_field} + """.format( + party_field=party_field, + party_name_field=party_name_field, + party_group_field=party_group_field, + territory_field=territory_field, + qty_field=qty_field, + date_field=self.date_field, + doctype=self.filters.doctype, + sales_person_field=sales_person_field, + sales_person_table=sales_person_table, + sales_person_condition=sales_person_condition, + supplier_table=supplier_table, + supplier_condition=supplier_condition, + is_opening_condition=is_opening_condition, + filter_conditions=filter_conditions + ), self.filters, as_dict=1) + + if self.filters.party_type == "Customer": + additional_customer_info = frappe.db.sql(""" + select + s.customer, GROUP_CONCAT(DISTINCT s.territory SEPARATOR ', ') as territory + {sales_person_field} + from + `tab{doctype} Item` i, `tab{doctype}` s {sales_person_table} + where i.parent = s.name and s.docstatus = 1 and s.company = %(company)s + and sp.parent = s.name and sp.parenttype = %(doctype)s + and s.{date_field} between %(from_date)s and %(to_date)s + {sales_person_condition} {is_opening_condition} {filter_conditions} + group by s.{party_field} + """.format( + party_field=party_field, + date_field=self.date_field, + doctype=self.filters.doctype, + sales_person_field=sales_person_field, + sales_person_table=sales_person_table, + sales_person_condition=sales_person_condition, + supplier_table=supplier_table, + supplier_condition=supplier_condition, + is_opening_condition=is_opening_condition, + filter_conditions=filter_conditions + ), self.filters, as_dict=1) + + for d in additional_customer_info: + self.additional_customer_info[d.customer] = d + + if self.entries: + self.itemsed_tax, self.tax_columns = get_tax_accounts(self.entries, [], self.company_currency, self.filters.doctype, + "Sales Taxes and Charges" if self.filters.party_type == "Customer" else "Purchase Taxes and Charges") + else: + self.itemsed_tax, self.tax_columns = {}, [] + + def get_qty_fieldname(self): + filter_to_field = { + "Stock Qty": "stock_qty", + "Contents Qty": "alt_uom_qty", + "Transaction Qty": "qty" + } + return filter_to_field.get(self.filters.qty_field, "stock_qty") + + def get_conditions(self): + conditions = [] + + if self.filters.get("customer"): + conditions.append("s.customer=%(customer)s") + + if self.filters.get("customer_group"): + lft, rgt = frappe.db.get_value("Customer Group", self.filters.customer_group, ["lft", "rgt"]) + conditions.append("""s.customer_group in (select name from `tabCustomer Group` + where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + + if self.filters.get("supplier"): + conditions.append("s.supplier=%(supplier)s") + + if self.filters.get("supplier_group"): + lft, rgt = frappe.db.get_value("Supplier Group", self.filters.supplier_group, ["lft", "rgt"]) + conditions.append("""sup.supplier_group in (select name from `tabSupplier Group` + where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + + if self.filters.get("item_code"): + conditions.append("i.item_code=%(item_code)s") + + if self.filters.get("item_group"): + lft, rgt = frappe.db.get_value("Item Group", self.filters.item_group, ["lft", "rgt"]) + conditions.append("""i.item_group in (select name from `tabItem Group` + where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + + if self.filters.get("brand"): + conditions.append("i.brand=%(brand)s") + + if self.filters.get("territory"): + lft, rgt = frappe.db.get_value("Territory", self.filters.territory, ["lft", "rgt"]) + conditions.append("""s.territory in (select name from `tabTerritory` + where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + + if self.filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", self.filters.sales_person, ["lft", "rgt"]) + conditions.append("""sp.sales_person in (select name from `tabSales Person` + where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + + return "and {}".format(" and ".join(conditions)) if conditions else "" + +def set_row_average_fields(row, tax_columns): + if not flt(row['qty']): + return + + fields = [ + ('base_net_rate', 'base_net_amount'), + ('base_rate', 'base_amount') + ] + for target, source in fields: + row[target] = flt(row[source]) / flt(row['qty']) + + tax_rate_fields = ["tax_" + scrub(tax) + "_rate" for tax in tax_columns] + for f in tax_rate_fields: + row[f] = row.get(f, 0.0) + if row[f+"_count"]: + row[f] /= row[f+"_count"] + + del row[f+"_count"] + +def execute(filters=None): + return SalesPurchaseDetailsReport(filters).run("Customer") From ad6ecbe903d9cc767054bcac778d74e25a790e36 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Mon, 25 Mar 2019 15:38:35 +0500 Subject: [PATCH 02/13] feat: Add Sales/Purchase Details Report link in explore links --- erpnext/config/buying.py | 6 ++++++ erpnext/config/selling.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/erpnext/config/buying.py b/erpnext/config/buying.py index e99b1d88aa55..5f32a179f440 100644 --- a/erpnext/config/buying.py +++ b/erpnext/config/buying.py @@ -139,6 +139,12 @@ def get_data(): "name": "Purchase Order Trends", "doctype": "Purchase Order" }, + { + "type": "report", + "is_query_report": True, + "name": "Purchase Details", + "doctype": "Purchase Order" + }, ] }, { diff --git a/erpnext/config/selling.py b/erpnext/config/selling.py index 99b8ce0e5ec5..6f569b4df6e7 100644 --- a/erpnext/config/selling.py +++ b/erpnext/config/selling.py @@ -215,6 +215,12 @@ def get_data(): "name": "Sales Order Trends", "doctype": "Sales Order" }, + { + "type": "report", + "is_query_report": True, + "name": "Sales Details", + "doctype": "Sales Order" + }, ] }, { From 646b4bfe4f379cb922e17facb83cff659f1b08c9 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Mon, 25 Mar 2019 16:13:23 +0500 Subject: [PATCH 03/13] feat(Sales/Purchase Details Report): Grand Total Column --- .../report/sales_details/sales_details.py | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py index 77d4f38ef4d6..09ca220e7770 100644 --- a/erpnext/selling/report/sales_details/sales_details.py +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -120,7 +120,7 @@ def get_columns(self): { "label": _("Total Tax Amount"), "fieldtype": "Currency", - "fieldname": "tax_total_amount", + "fieldname": "total_tax_amount", "options": "currency", "width": 120 }, @@ -145,6 +145,16 @@ def get_columns(self): }, ] + columns += [ + { + "label": _("Grand Total"), + "fieldtype": "Currency", + "fieldname": "grand_total", + "options": "currency", + "width": 120 + }, + ] + if self.filters.party_type == "Customer": columns += [ { @@ -190,29 +200,30 @@ def get_columns(self): def get_data(self): self.get_entries() + self.get_itemsed_taxes() self.build_tree() data = [] self.total_row["indent"] = 0 self.total_row["_collapsed"] = True - set_row_average_fields(self.total_row, self.tax_columns) + self.postprocess_row(self.total_row) data.append(self.total_row) for party, docs in iteritems(self.tree): party_row = self.party_totals[party] - set_row_average_fields(party_row, self.tax_columns) + self.postprocess_row(party_row) party_row["indent"] = 1 data.append(party_row) for docname, items_uoms in iteritems(docs): doc_row = self.doc_totals[docname] - set_row_average_fields(doc_row, self.tax_columns) + self.postprocess_row(doc_row) doc_row["indent"] = 2 data.append(doc_row) for item_code, uom in items_uoms: item_row = self.doc_item_uom_totals[(docname, item_code, uom)] - set_row_average_fields(item_row, self.tax_columns) + self.postprocess_row(item_row) item_row["indent"] = 3 data.append(item_row) @@ -221,14 +232,12 @@ def get_data(self): def build_tree(self): # Totals Row Template total_fields = ['qty', 'base_net_amount', 'base_amount'] - tax_amount_fields = ["tax_" + scrub(tax) for tax in self.tax_columns] - tax_rate_fields = ["tax_" + scrub(tax) + "_rate" for tax in self.tax_columns] - totals_template = {"currency": self.company_currency, "tax_total_amount": 0.0} + totals_template = {"currency": self.company_currency} for f in total_fields: totals_template[f] = 0.0 - for f in tax_amount_fields + tax_rate_fields: + for f in self.tax_amount_fields + self.tax_rate_fields: totals_template[f] = 0.0 - for f in tax_rate_fields: + for f in self.tax_rate_fields: totals_template[f+"_count"] = 0 # Containers @@ -316,17 +325,13 @@ def build_tree(self): item_row[f] += d[f] self.total_row[f] += d[f] - for f, tax in zip(tax_amount_fields, self.tax_columns): + for f, tax in zip(self.tax_amount_fields, self.tax_columns): tax_amount = self.itemsed_tax.get(d.name, {}).get(tax, {}).get("tax_amount", 0.0) party_row[f] += tax_amount - party_row["tax_total_amount"] += tax_amount doc_row[f] += tax_amount - doc_row["tax_total_amount"] += tax_amount item_row[f] += tax_amount - item_row["tax_total_amount"] += tax_amount self.total_row[f] += tax_amount - self.total_row["tax_total_amount"] += tax_amount - for f, tax in zip(tax_rate_fields, self.tax_columns): + for f, tax in zip(self.tax_rate_fields, self.tax_columns): tax_rate = self.itemsed_tax.get(d.name, {}).get(tax, {}).get("tax_rate", 0.0) if tax_rate: party_row[f] += tax_rate @@ -422,11 +427,40 @@ def get_entries(self): for d in additional_customer_info: self.additional_customer_info[d.customer] = d + def get_itemsed_taxes(self): if self.entries: self.itemsed_tax, self.tax_columns = get_tax_accounts(self.entries, [], self.company_currency, self.filters.doctype, "Sales Taxes and Charges" if self.filters.party_type == "Customer" else "Purchase Taxes and Charges") + self.tax_amount_fields = ["tax_" + scrub(tax) for tax in self.tax_columns] + self.tax_rate_fields = ["tax_" + scrub(tax) + "_rate" for tax in self.tax_columns] else: self.itemsed_tax, self.tax_columns = {}, [] + self.tax_amount_fields, self.tax_rate_fields = [], [] + + def postprocess_row(self, row): + # Calculate rate + rate_fields = [ + ('base_net_rate', 'base_net_amount'), + ('base_rate', 'base_amount') + ] + if flt(row['qty']): + for target, source in rate_fields: + row[target] = flt(row[source]) / flt(row['qty']) + + # Calculate total taxes and grand total + row["total_tax_amount"] = 0.0 + for f in self.tax_amount_fields: + row["total_tax_amount"] += row[f] + + row["grand_total"] = row["base_net_amount"] + row["total_tax_amount"] + + # Calculate tax rates by averaging + for f in self.tax_rate_fields: + row[f] = row.get(f, 0.0) + if row[f + "_count"]: + row[f] /= row[f + "_count"] + + del row[f + "_count"] def get_qty_fieldname(self): filter_to_field = { @@ -478,24 +512,5 @@ def get_conditions(self): return "and {}".format(" and ".join(conditions)) if conditions else "" -def set_row_average_fields(row, tax_columns): - if not flt(row['qty']): - return - - fields = [ - ('base_net_rate', 'base_net_amount'), - ('base_rate', 'base_amount') - ] - for target, source in fields: - row[target] = flt(row[source]) / flt(row['qty']) - - tax_rate_fields = ["tax_" + scrub(tax) + "_rate" for tax in tax_columns] - for f in tax_rate_fields: - row[f] = row.get(f, 0.0) - if row[f+"_count"]: - row[f] /= row[f+"_count"] - - del row[f+"_count"] - def execute(filters=None): return SalesPurchaseDetailsReport(filters).run("Customer") From 6ba6324551763fdc655e91f59e2f392c22292ad3 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Mon, 25 Mar 2019 16:13:56 +0500 Subject: [PATCH 04/13] fix(Item Wise Sales Register): Do not round amounts in get_tax_accounts --- .../item_wise_sales_register.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 88c612e7f6c5..561c7d4f963b 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -186,9 +186,6 @@ def get_tax_accounts(item_list, columns, company_currency, invoice_item_row = {} itemised_tax = {} - tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"), - currency=company_currency) or 2 - for d in item_list: invoice_item_row.setdefault(d.parent, []).append(d) item_row_map.setdefault(d.parent, {}).setdefault(d.item_code or d.item_name, []).append(d) @@ -241,9 +238,8 @@ def get_tax_accounts(item_list, columns, company_currency, item_tax_amount = flt((tax_amount * d.base_net_amount) / item_net_amount) \ if item_net_amount else 0 if item_tax_amount: - tax_value = flt(item_tax_amount, tax_amount_precision) - tax_value = (tax_value * -1 - if (doctype == 'Purchase Invoice' and name in deducted_tax) else tax_value) + tax_value = (item_tax_amount * -1 + if (doctype == 'Purchase Invoice' and name in deducted_tax) else item_tax_amount) itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ "tax_rate": tax_rate, @@ -256,8 +252,7 @@ def get_tax_accounts(item_list, columns, company_currency, for d in invoice_item_row.get(parent, []): itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ "tax_rate": "NA", - "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total, - tax_amount_precision) + "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total) }) tax_columns.sort() From 07a97b1f4725d49a06242224b780228daa82dee2 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Wed, 27 Mar 2019 13:03:30 +0500 Subject: [PATCH 05/13] fix(Sales/Purchase Details Report): Always show name column --- .../report/sales_details/sales_details.py | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py index 09ca220e7770..8bd0a4f6964c 100644 --- a/erpnext/selling/report/sales_details/sales_details.py +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -35,35 +35,20 @@ def run(self, party_type): return columns, data def get_columns(self): - show_name = False - if self.filters.tree_type == "Customer": - if frappe.defaults.get_global_default('cust_master_name') == "Naming Series": - show_name = True - if self.filters.tree_type == "Supplier": - if frappe.defaults.get_global_default('supp_master_name') == "Naming Series": - show_name = True - if frappe.defaults.get_global_default('item_naming_by') == "Naming Series": - show_name = True - columns = [ { "label": _("Reference"), "fieldtype": "Dynamic Link", - "fieldname": "docname", + "fieldname": "reference", "options": "doctype", - "width": 400 - } - ] - - if show_name: - columns.append({ + "width": 300 + }, + { "label": _("Name"), "fieldtype": "Data", - "fieldname": "name", + "fieldname": "reference_name", "width": 150 - }) - - columns += [ + }, { "label": _("Type"), "fieldtype": "Data", @@ -245,7 +230,7 @@ def build_tree(self): self.party_totals = {} self.doc_totals = {} self.doc_item_uom_totals = {} - self.total_row = {"docname": _("'Total'")} + self.total_row = {"reference": _("'Total'")} self.total_row.update(totals_template) # Build tree and group totals @@ -268,8 +253,8 @@ def build_tree(self): party_row = self.party_totals[d.party] = totals_template.copy() party_row.update({ "doctype": self.filters.party_type, - "docname": d.party, - "name": d.party_name, + "reference": d.party, + "reference_name": d.party_name, "group": d.party_group, "group_doctype": d.party_group_dt }) @@ -288,7 +273,7 @@ def build_tree(self): doc_row.update({ "date": d.date, "doctype": self.filters.doctype, - "docname": d.parent + "reference": d.parent }) if self.filters.party_type == "Customer": doc_row.update({ @@ -304,8 +289,8 @@ def build_tree(self): item_row.update({ "date": d.date, "doctype": "Item", - "docname": d.item_code, - "name": d.item_name, + "reference": d.item_code, + "reference_name": d.item_name, "uom": d.uom, "group": d.item_group, "group_doctype": "Item Group", From 840660ad6fbed881969c4382549d67f9550ec415 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Sat, 6 Apr 2019 21:54:25 +0500 Subject: [PATCH 06/13] feat(Sales Details Report): List view and Filter to show tax columns --- .../purchase_details/purchase_details.js | 37 +- .../report/sales_details/sales_details.js | 37 +- .../report/sales_details/sales_details.py | 341 ++++++++++-------- 3 files changed, 247 insertions(+), 168 deletions(-) diff --git a/erpnext/buying/report/purchase_details/purchase_details.js b/erpnext/buying/report/purchase_details/purchase_details.js index 40a4ff5b5ee6..d3ecd21871e5 100644 --- a/erpnext/buying/report/purchase_details/purchase_details.js +++ b/erpnext/buying/report/purchase_details/purchase_details.js @@ -13,17 +13,11 @@ frappe.query_reports["Purchase Details"] = { reqd: 1 }, { - fieldname: "from_date", - label: __("From Date"), - fieldtype: "Date", - default: frappe.defaults.get_user_default("year_start_date"), - reqd: 1 - }, - { - fieldname:"to_date", - label: __("To Date"), - fieldtype: "Date", - default: frappe.defaults.get_user_default("year_end_date"), + fieldname: "view", + label: __("View Type"), + fieldtype: "Select", + options: ["Tree", "List"], + default: "Tree", reqd: 1 }, { @@ -42,6 +36,20 @@ frappe.query_reports["Purchase Details"] = { default: "Stock Qty", reqd: 1 }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_end_date"), + reqd: 1 + }, { fieldname: "supplier", label: __("Supplier"), @@ -71,6 +79,11 @@ frappe.query_reports["Purchase Details"] = { label: __("Brand"), fieldtype: "Link", options: "Brand" - } + }, + { + fieldname: "include_taxes", + label: __("Include Taxes"), + fieldtype: "Check" + }, ], } diff --git a/erpnext/selling/report/sales_details/sales_details.js b/erpnext/selling/report/sales_details/sales_details.js index c72986922726..bf0b82f9be65 100644 --- a/erpnext/selling/report/sales_details/sales_details.js +++ b/erpnext/selling/report/sales_details/sales_details.js @@ -13,17 +13,11 @@ frappe.query_reports["Sales Details"] = { reqd: 1 }, { - fieldname: "from_date", - label: __("From Date"), - fieldtype: "Date", - default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), - reqd: 1 - }, - { - fieldname: "to_date", - label: __("To Date"), - fieldtype: "Date", - default: frappe.datetime.get_today(), + fieldname: "view", + label: __("View Type"), + fieldtype: "Select", + options: ["Tree", "List"], + default: "Tree", reqd: 1 }, { @@ -42,6 +36,20 @@ frappe.query_reports["Sales Details"] = { default: "Stock Qty", reqd: 1 }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + }, { fieldname: "customer", label: __("Customer"), @@ -83,7 +91,12 @@ frappe.query_reports["Sales Details"] = { label: __("Sales Person"), fieldtype: "Link", options: "Sales Person" - } + }, + { + fieldname: "include_taxes", + label: __("Include Taxes"), + fieldtype: "Check" + }, ], } diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py index 8bd0a4f6964c..705111a08381 100644 --- a/erpnext/selling/report/sales_details/sales_details.py +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -35,32 +35,66 @@ def run(self, party_type): return columns, data def get_columns(self): - columns = [ - { - "label": _("Reference"), - "fieldtype": "Dynamic Link", - "fieldname": "reference", - "options": "doctype", - "width": 300 - }, - { - "label": _("Name"), - "fieldtype": "Data", - "fieldname": "reference_name", - "width": 150 - }, - { - "label": _("Type"), - "fieldtype": "Data", - "fieldname": "doctype", - "width": 110 - }, - { - "label": _("Date"), - "fieldtype": "Date", - "fieldname": "date", - "width": 80 - }, + if self.filters.view == "Tree": + columns = [ + { + "label": _("Reference"), + "fieldtype": "Dynamic Link", + "fieldname": "reference", + "options": "doctype", + "width": 300 + }, + { + "label": _("Name"), + "fieldtype": "Data", + "fieldname": "reference_name", + "width": 150 + }, + { + "label": _("Type"), + "fieldtype": "Data", + "fieldname": "doctype", + "width": 110 + }, + { + "label": _("Date"), + "fieldtype": "Date", + "fieldname": "date", + "width": 80 + }, + ] + else: + columns = [ + { + "label": _("Date"), + "fieldtype": "Date", + "fieldname": "date", + "width": 80 + }, + { + "label": _("Voucher No"), + "fieldtype": "Link", + "fieldname": "voucher_no", + "options": self.filters.doctype, + "width": 140 + }, + { + "label": _(self.filters.party_type), + "fieldtype": "Link", + "fieldname": "party", + "options": self.filters.party_type, + "width": 150 + }, + { + "label": _("Item"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 150 + }, + ] + + columns += [ { "label": _("UOM"), "fieldtype": "Link", @@ -78,68 +112,66 @@ def get_columns(self): "label": _("Net Rate"), "fieldtype": "Currency", "fieldname": "base_net_rate", - "options": "currency", + "options": "Company:company:default_currency", "width": 120 }, { "label": _("Net Amount"), "fieldtype": "Currency", "fieldname": "base_net_amount", - "options": "currency", + "options": "Company:company:default_currency", "width": 120 }, { "label": _("Rate"), "fieldtype": "Currency", "fieldname": "base_rate", - "options": "currency", + "options": "Company:company:default_currency", "width": 120 }, { "label": _("Amount"), "fieldtype": "Currency", "fieldname": "base_amount", - "options": "currency", + "options": "Company:company:default_currency", "width": 120 }, { - "label": _("Total Tax Amount"), + "label": _("Taxes and Charges"), "fieldtype": "Currency", "fieldname": "total_tax_amount", - "options": "currency", + "options": "Company:company:default_currency", "width": 120 }, - ] - - for tax_description in self.tax_columns: - amount_field = "tax_" + scrub(tax_description) - rate_field = amount_field + "_rate" - columns += [ - { - "label": _(tax_description) + " (%)", - "fieldtype": "Percent", - "fieldname": rate_field, - "width": 60 - }, - { - "label": _(tax_description), - "fieldtype": "Currency", - "fieldname": amount_field, - "options": "currency", - "width": 120 - }, - ] - - columns += [ { "label": _("Grand Total"), "fieldtype": "Currency", "fieldname": "grand_total", - "options": "currency", + "options": "Company:company:default_currency", "width": 120 }, ] + if self.filters.include_taxes: + for tax_description in self.tax_columns: + amount_field = "tax_" + scrub(tax_description) + rate_field = amount_field + "_rate" + columns += [ + { + "label": _(tax_description) + " (%)", + "fieldtype": "Percent", + "fieldname": rate_field, + "width": 60 + }, + { + "label": _(tax_description), + "fieldtype": "Currency", + "fieldname": amount_field, + "options": "Company:company:default_currency", + "width": 120 + }, + ] + if self.filters.party_type == "Customer": columns += [ { @@ -172,49 +204,48 @@ def get_columns(self): "options": "Brand", "width": 100 }, - { - "label": _("Currency"), - "fieldtype": "Link", - "fieldname": "currency", - "options": "Currency", - "width": 50 - }, ] return columns def get_data(self): self.get_entries() - self.get_itemsed_taxes() - self.build_tree() + self.get_itemised_taxes() + self.prepare_data() data = [] - self.total_row["indent"] = 0 - self.total_row["_collapsed"] = True - self.postprocess_row(self.total_row) - data.append(self.total_row) - - for party, docs in iteritems(self.tree): - party_row = self.party_totals[party] - self.postprocess_row(party_row) - party_row["indent"] = 1 - data.append(party_row) - - for docname, items_uoms in iteritems(docs): - doc_row = self.doc_totals[docname] - self.postprocess_row(doc_row) - doc_row["indent"] = 2 - data.append(doc_row) - - for item_code, uom in items_uoms: - item_row = self.doc_item_uom_totals[(docname, item_code, uom)] - self.postprocess_row(item_row) - item_row["indent"] = 3 - data.append(item_row) + + if self.filters.view == "Tree": + self.total_row["indent"] = 0 + self.total_row["_collapsed"] = True + self.postprocess_row(self.total_row) + + data.append(self.total_row) + for party, docs in iteritems(self.tree): + party_row = self.party_totals[party] + self.postprocess_row(party_row) + party_row["indent"] = 1 + data.append(party_row) + + for docname, items_uoms in iteritems(docs): + doc_row = self.doc_totals[docname] + self.postprocess_row(doc_row) + doc_row["indent"] = 2 + data.append(doc_row) + + for item_code, uom in items_uoms: + item_row = self.doc_item_uom_totals[(docname, item_code, uom)] + self.postprocess_row(item_row) + item_row["indent"] = 3 + data.append(item_row) + else: + for item in self.item_list: + self.postprocess_row(item) + data = self.item_list return data - def build_tree(self): + def prepare_data(self): # Totals Row Template total_fields = ['qty', 'base_net_amount', 'base_amount'] totals_template = {"currency": self.company_currency} @@ -227,6 +258,7 @@ def build_tree(self): # Containers self.tree = OrderedDict() + self.item_list = [] self.party_totals = {} self.doc_totals = {} self.doc_item_uom_totals = {} @@ -243,90 +275,111 @@ def build_tree(self): else: d.uom = d.stock_uom - # Add tree nodes if not already there - self.tree.setdefault(d.party, OrderedDict())\ - .setdefault(d.parent, set())\ - .add((d.item_code, d.uom)) - - # Party total row - if d.party not in self.party_totals: - party_row = self.party_totals[d.party] = totals_template.copy() - party_row.update({ - "doctype": self.filters.party_type, - "reference": d.party, - "reference_name": d.party_name, - "group": d.party_group, - "group_doctype": d.party_group_dt - }) - if self.filters.party_type == "Customer": - details = self.additional_customer_info.get(d.party, frappe._dict()) + if self.filters.view == "Tree": + # Add tree nodes if not already there + self.tree.setdefault(d.party, OrderedDict())\ + .setdefault(d.parent, set())\ + .add((d.item_code, d.uom)) + + # Party total row + if d.party not in self.party_totals: + party_row = self.party_totals[d.party] = totals_template.copy() party_row.update({ - "sales_person": details.sales_person, - "territory": details.territory + "doctype": self.filters.party_type, + "reference": d.party, + "reference_name": d.party_name, + "group": d.party_group, + "group_doctype": d.party_group_dt }) - else: - party_row = self.party_totals[d.party] - - # Document total row - if d.parent not in self.doc_totals: - doc_row = self.doc_totals[d.parent] = totals_template.copy() - doc_row.update({ - "date": d.date, - "doctype": self.filters.doctype, - "reference": d.parent - }) - if self.filters.party_type == "Customer": + if self.filters.party_type == "Customer": + details = self.additional_customer_info.get(d.party, frappe._dict()) + party_row.update({ + "sales_person": details.sales_person, + "territory": details.territory + }) + else: + party_row = self.party_totals[d.party] + + # Document total row + if d.parent not in self.doc_totals: + doc_row = self.doc_totals[d.parent] = totals_template.copy() doc_row.update({ - "sales_person": d.sales_person, - "territory": d.territory + "date": d.date, + "doctype": self.filters.doctype, + "reference": d.parent }) - else: - doc_row = self.doc_totals[d.parent] + if self.filters.party_type == "Customer": + doc_row.update({ + "sales_person": d.sales_person, + "territory": d.territory + }) + else: + doc_row = self.doc_totals[d.parent] # Doc-Item-UOM row if (d.parent, d.item_code, d.uom) not in self.doc_item_uom_totals: item_row = self.doc_item_uom_totals[(d.parent, d.item_code, d.uom)] = totals_template.copy() item_row.update({ "date": d.date, - "doctype": "Item", - "reference": d.item_code, - "reference_name": d.item_name, "uom": d.uom, "group": d.item_group, "group_doctype": "Item Group", "brand": d.brand }) + if self.filters.view == "Tree": + item_row.update({ + "doctype": "Item", + "reference": d.item_code, + "reference_name": d.item_name, + }) + else: + item_row.update({ + "voucher_no": d.parent, + "party": d.party, + "party_name": d.party_name, + "item_code": d.item_code, + "item_name": d.item_name + }) + if self.filters.party_type == "Customer": item_row.update({ "sales_person": d.sales_person, "territory": d.territory }) + + if self.filters.view != "Tree": + self.item_list.append(item_row) + else: item_row = self.doc_item_uom_totals[(d.parent, d.item_code, d.uom)] + # Group totals for f in total_fields: - party_row[f] += d[f] - doc_row[f] += d[f] item_row[f] += d[f] - self.total_row[f] += d[f] + if self.filters.view == "Tree": + party_row[f] += d[f] + doc_row[f] += d[f] + self.total_row[f] += d[f] for f, tax in zip(self.tax_amount_fields, self.tax_columns): - tax_amount = self.itemsed_tax.get(d.name, {}).get(tax, {}).get("tax_amount", 0.0) - party_row[f] += tax_amount - doc_row[f] += tax_amount + tax_amount = self.itemised_tax.get(d.name, {}).get(tax, {}).get("tax_amount", 0.0) item_row[f] += tax_amount - self.total_row[f] += tax_amount + if self.filters.view == "Tree": + doc_row[f] += tax_amount + party_row[f] += tax_amount + self.total_row[f] += tax_amount for f, tax in zip(self.tax_rate_fields, self.tax_columns): - tax_rate = self.itemsed_tax.get(d.name, {}).get(tax, {}).get("tax_rate", 0.0) + tax_rate = self.itemised_tax.get(d.name, {}).get(tax, {}).get("tax_rate", 0.0) if tax_rate: - party_row[f] += tax_rate - party_row[f+"_count"] += 1 - doc_row[f] += tax_rate - doc_row[f+"_count"] += 1 item_row[f] += tax_rate item_row[f+"_count"] += 1 - self.total_row[f] += tax_rate - self.total_row[f+"_count"] += 1 + if self.filters.view == "Tree": + doc_row[f] += tax_rate + doc_row[f+"_count"] += 1 + party_row[f] += tax_rate + party_row[f+"_count"] += 1 + self.total_row[f] += tax_rate + self.total_row[f+"_count"] += 1 def get_entries(self): party_field = scrub(self.filters.party_type) @@ -366,7 +419,7 @@ def get_entries(self): and s.{date_field} between %(from_date)s and %(to_date)s {sales_person_condition} {supplier_condition} {is_opening_condition} {filter_conditions} group by s.name, i.name - order by s.{date_field} + order by s.{date_field}, s.{party_field}, s.name, i.item_code """.format( party_field=party_field, party_name_field=party_name_field, @@ -384,7 +437,7 @@ def get_entries(self): filter_conditions=filter_conditions ), self.filters, as_dict=1) - if self.filters.party_type == "Customer": + if self.filters.party_type == "Customer" and self.filters.view == "Tree": additional_customer_info = frappe.db.sql(""" select s.customer, GROUP_CONCAT(DISTINCT s.territory SEPARATOR ', ') as territory @@ -412,14 +465,14 @@ def get_entries(self): for d in additional_customer_info: self.additional_customer_info[d.customer] = d - def get_itemsed_taxes(self): + def get_itemised_taxes(self): if self.entries: - self.itemsed_tax, self.tax_columns = get_tax_accounts(self.entries, [], self.company_currency, self.filters.doctype, + self.itemised_tax, self.tax_columns = get_tax_accounts(self.entries, [], self.company_currency, self.filters.doctype, "Sales Taxes and Charges" if self.filters.party_type == "Customer" else "Purchase Taxes and Charges") self.tax_amount_fields = ["tax_" + scrub(tax) for tax in self.tax_columns] self.tax_rate_fields = ["tax_" + scrub(tax) + "_rate" for tax in self.tax_columns] else: - self.itemsed_tax, self.tax_columns = {}, [] + self.itemised_tax, self.tax_columns = {}, [] self.tax_amount_fields, self.tax_rate_fields = [], [] def postprocess_row(self, row): From 21236aec21776bb67c0cf1e28559d89144316df8 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Sun, 7 Apr 2019 16:32:30 +0500 Subject: [PATCH 07/13] fix(Sales/Purchase Details): Remove unnecessary columns and use formatter --- .../report/sales_details/sales_details.py | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py index 705111a08381..1eb4bc83f806 100644 --- a/erpnext/selling/report/sales_details/sales_details.py +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -41,19 +41,13 @@ def get_columns(self): "label": _("Reference"), "fieldtype": "Dynamic Link", "fieldname": "reference", - "options": "doctype", + "options": "doc_type", "width": 300 }, - { - "label": _("Name"), - "fieldtype": "Data", - "fieldname": "reference_name", - "width": 150 - }, { "label": _("Type"), "fieldtype": "Data", - "fieldname": "doctype", + "fieldname": "doc_type", "width": 110 }, { @@ -122,20 +116,6 @@ def get_columns(self): "options": "Company:company:default_currency", "width": 120 }, - { - "label": _("Rate"), - "fieldtype": "Currency", - "fieldname": "base_rate", - "options": "Company:company:default_currency", - "width": 120 - }, - { - "label": _("Amount"), - "fieldtype": "Currency", - "fieldname": "base_amount", - "options": "Company:company:default_currency", - "width": 120 - }, { "label": _("Taxes and Charges"), "fieldtype": "Currency", @@ -285,9 +265,10 @@ def prepare_data(self): if d.party not in self.party_totals: party_row = self.party_totals[d.party] = totals_template.copy() party_row.update({ - "doctype": self.filters.party_type, + "doc_type": self.filters.party_type, "reference": d.party, - "reference_name": d.party_name, + scrub(self.filters.party_type) + "_name": d.party_name, + "party_name": d.party_name, "group": d.party_group, "group_doctype": d.party_group_dt }) @@ -305,7 +286,7 @@ def prepare_data(self): doc_row = self.doc_totals[d.parent] = totals_template.copy() doc_row.update({ "date": d.date, - "doctype": self.filters.doctype, + "doc_type": self.filters.doctype, "reference": d.parent }) if self.filters.party_type == "Customer": @@ -328,15 +309,16 @@ def prepare_data(self): }) if self.filters.view == "Tree": item_row.update({ - "doctype": "Item", + "doc_type": "Item", "reference": d.item_code, - "reference_name": d.item_name, + "item_name": d.item_name, }) else: item_row.update({ "voucher_no": d.parent, "party": d.party, "party_name": d.party_name, + scrub(self.filters.party_type) + "_name": d.party_name, "item_code": d.item_code, "item_name": d.item_name }) From 522ab14f95cfb1b377e3cac06950b3cf4a09c5ca Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Mon, 8 Apr 2019 11:47:35 +0500 Subject: [PATCH 08/13] fix(Sales/Purchase Details): Collapse tree onload --- erpnext/buying/report/purchase_details/purchase_details.js | 5 +++++ erpnext/selling/report/sales_details/sales_details.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/erpnext/buying/report/purchase_details/purchase_details.js b/erpnext/buying/report/purchase_details/purchase_details.js index d3ecd21871e5..a63c13b2e555 100644 --- a/erpnext/buying/report/purchase_details/purchase_details.js +++ b/erpnext/buying/report/purchase_details/purchase_details.js @@ -86,4 +86,9 @@ frappe.query_reports["Purchase Details"] = { fieldtype: "Check" }, ], + after_datatable_render: function(datatable_obj) { + if(frappe.query_report.get_filter_value('view') == "Tree") { + datatable_obj.rowmanager.collapseAllNodes(); + } + }, } diff --git a/erpnext/selling/report/sales_details/sales_details.js b/erpnext/selling/report/sales_details/sales_details.js index bf0b82f9be65..e11e2538caf9 100644 --- a/erpnext/selling/report/sales_details/sales_details.js +++ b/erpnext/selling/report/sales_details/sales_details.js @@ -98,6 +98,11 @@ frappe.query_reports["Sales Details"] = { fieldtype: "Check" }, ], + after_datatable_render: function(datatable_obj) { + if(frappe.query_report.get_filter_value('view') == "Tree") { + datatable_obj.rowmanager.collapseAllNodes(); + } + }, } From d99cf17270122dfd222c9e49a469c716f220d037 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 9 Apr 2019 18:18:19 +0500 Subject: [PATCH 09/13] feat(Sales/Purchase Details): Use initial_depth 1 instead of collapse all --- erpnext/buying/report/purchase_details/purchase_details.js | 6 +----- erpnext/selling/report/sales_details/sales_details.js | 6 +----- erpnext/selling/report/sales_details/sales_details.py | 1 - 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/erpnext/buying/report/purchase_details/purchase_details.js b/erpnext/buying/report/purchase_details/purchase_details.js index a63c13b2e555..eef5673ecec3 100644 --- a/erpnext/buying/report/purchase_details/purchase_details.js +++ b/erpnext/buying/report/purchase_details/purchase_details.js @@ -86,9 +86,5 @@ frappe.query_reports["Purchase Details"] = { fieldtype: "Check" }, ], - after_datatable_render: function(datatable_obj) { - if(frappe.query_report.get_filter_value('view') == "Tree") { - datatable_obj.rowmanager.collapseAllNodes(); - } - }, + "initial_depth": 1 } diff --git a/erpnext/selling/report/sales_details/sales_details.js b/erpnext/selling/report/sales_details/sales_details.js index e11e2538caf9..7c8a7e5ab308 100644 --- a/erpnext/selling/report/sales_details/sales_details.js +++ b/erpnext/selling/report/sales_details/sales_details.js @@ -98,11 +98,7 @@ frappe.query_reports["Sales Details"] = { fieldtype: "Check" }, ], - after_datatable_render: function(datatable_obj) { - if(frappe.query_report.get_filter_value('view') == "Tree") { - datatable_obj.rowmanager.collapseAllNodes(); - } - }, + "initial_depth": 1 } diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py index 1eb4bc83f806..cf6e739b272b 100644 --- a/erpnext/selling/report/sales_details/sales_details.py +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -197,7 +197,6 @@ def get_data(self): if self.filters.view == "Tree": self.total_row["indent"] = 0 - self.total_row["_collapsed"] = True self.postprocess_row(self.total_row) data.append(self.total_row) From 99eead9c9a7b8e3739b9befb2bfded8a4ed98965 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Fri, 26 Apr 2019 15:17:25 +0500 Subject: [PATCH 10/13] fix(Sales/Purchase Details): Handle the case that tax_rate can be "NA" --- erpnext/selling/report/sales_details/sales_details.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py index cf6e739b272b..dfd4bc6fb48d 100644 --- a/erpnext/selling/report/sales_details/sales_details.py +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -351,6 +351,8 @@ def prepare_data(self): self.total_row[f] += tax_amount for f, tax in zip(self.tax_rate_fields, self.tax_columns): tax_rate = self.itemised_tax.get(d.name, {}).get(tax, {}).get("tax_rate", 0.0) + if tax_rate == "NA": + tax_rate = 0.0 if tax_rate: item_row[f] += tax_rate item_row[f+"_count"] += 1 From 8cd1eb5c647e251ff36fdd4cfdd563015d7d75ae Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 30 Apr 2019 11:33:19 +0500 Subject: [PATCH 11/13] fix(Sales/Purchase Details): Remove letter head in report json --- erpnext/buying/report/purchase_details/purchase_details.json | 5 ++--- erpnext/selling/report/sales_details/sales_details.json | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/buying/report/purchase_details/purchase_details.json b/erpnext/buying/report/purchase_details/purchase_details.json index 656cb6c8203b..612edebcdc83 100644 --- a/erpnext/buying/report/purchase_details/purchase_details.json +++ b/erpnext/buying/report/purchase_details/purchase_details.json @@ -6,8 +6,7 @@ "docstatus": 0, "doctype": "Report", "idx": 0, - "is_standard": "Yes", - "letter_head": "Techno Automotive Refinish", + "is_standard": "Yes", "modified": "2019-03-25 15:25:54.566163", "modified_by": "Administrator", "module": "Buying", @@ -25,4 +24,4 @@ "role": "Accounts User" } ] -} \ No newline at end of file +} diff --git a/erpnext/selling/report/sales_details/sales_details.json b/erpnext/selling/report/sales_details/sales_details.json index 0c80ac48d45b..19cf25ca158c 100644 --- a/erpnext/selling/report/sales_details/sales_details.json +++ b/erpnext/selling/report/sales_details/sales_details.json @@ -6,8 +6,7 @@ "docstatus": 0, "doctype": "Report", "idx": 0, - "is_standard": "Yes", - "letter_head": "Techno Automotive Refinish", + "is_standard": "Yes", "modified": "2019-03-24 16:27:15.822005", "modified_by": "Administrator", "module": "Selling", @@ -25,4 +24,4 @@ "role": "Sales User" } ] -} \ No newline at end of file +} From ea7eb87cf0791fb642f447ec42a7d014256f36f0 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Thu, 23 May 2019 16:52:28 +0500 Subject: [PATCH 12/13] feat(Sales/Purchase Details): Dynamic grouping using grouping utility --- .../purchase_details/purchase_details.js | 44 +- .../report/sales_details/sales_details.js | 38 +- .../report/sales_details/sales_details.py | 664 +++++++++--------- 3 files changed, 390 insertions(+), 356 deletions(-) diff --git a/erpnext/buying/report/purchase_details/purchase_details.js b/erpnext/buying/report/purchase_details/purchase_details.js index eef5673ecec3..172f9859ae25 100644 --- a/erpnext/buying/report/purchase_details/purchase_details.js +++ b/erpnext/buying/report/purchase_details/purchase_details.js @@ -12,14 +12,6 @@ frappe.query_reports["Purchase Details"] = { default: frappe.defaults.get_user_default("Company"), reqd: 1 }, - { - fieldname: "view", - label: __("View Type"), - fieldtype: "Select", - options: ["Tree", "List"], - default: "Tree", - reqd: 1 - }, { fieldname: "doctype", label: __("Based On"), @@ -40,14 +32,14 @@ frappe.query_reports["Purchase Details"] = { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", - default: frappe.defaults.get_user_default("year_start_date"), + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), reqd: 1 }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.defaults.get_user_default("year_end_date"), + default: frappe.datetime.get_today(), reqd: 1 }, { @@ -80,6 +72,36 @@ frappe.query_reports["Purchase Details"] = { fieldtype: "Link", options: "Brand" }, + { + fieldname: "group_by_1", + label: __("Group By Level 1"), + fieldtype: "Select", + options: ["Ungrouped", "Group by Supplier", "Group by Supplier Group", "Group by Transaction", + "Group by Item", "Group by Item Group", "Group by Brand"], + default: "Ungrouped" + }, + { + fieldname: "group_by_2", + label: __("Group By Level 2"), + fieldtype: "Select", + options: ["Ungrouped", "Group by Supplier", "Group by Supplier Group", "Group by Transaction", + "Group by Item", "Group by Item Group", "Group by Brand"], + default: "Group by Supplier" + }, + { + fieldname: "group_by_3", + label: __("Group By Level 3"), + fieldtype: "Select", + options: ["Ungrouped", "Group by Supplier", "Group by Supplier Group", "Group by Transaction", + "Group by Item", "Group by Item Group", "Group by Brand"], + default: "Group by Transaction" + }, + { + fieldname: "group_same_items", + label: __("Group Same Items"), + fieldtype: "Check", + default: 1 + }, { fieldname: "include_taxes", label: __("Include Taxes"), diff --git a/erpnext/selling/report/sales_details/sales_details.js b/erpnext/selling/report/sales_details/sales_details.js index 7c8a7e5ab308..ce63aae23499 100644 --- a/erpnext/selling/report/sales_details/sales_details.js +++ b/erpnext/selling/report/sales_details/sales_details.js @@ -12,14 +12,6 @@ frappe.query_reports["Sales Details"] = { default: frappe.defaults.get_user_default("Company"), reqd: 1 }, - { - fieldname: "view", - label: __("View Type"), - fieldtype: "Select", - options: ["Tree", "List"], - default: "Tree", - reqd: 1 - }, { fieldname: "doctype", label: __("Based On"), @@ -92,6 +84,36 @@ frappe.query_reports["Sales Details"] = { fieldtype: "Link", options: "Sales Person" }, + { + fieldname: "group_by_1", + label: __("Group By Level 1"), + fieldtype: "Select", + options: ["Ungrouped", "Group by Customer", "Group by Customer Group", "Group by Transaction", + "Group by Item", "Group by Item Group", "Group by Brand", "Group by Territory", "Group by Sales Person"], + default: "Ungrouped" + }, + { + fieldname: "group_by_2", + label: __("Group By Level 2"), + fieldtype: "Select", + options: ["Ungrouped", "Group by Customer", "Group by Customer Group", "Group by Transaction", + "Group by Item", "Group by Item Group", "Group by Brand", "Group by Territory", "Group by Sales Person"], + default: "Group by Customer" + }, + { + fieldname: "group_by_3", + label: __("Group By Level 3"), + fieldtype: "Select", + options: ["Ungrouped", "Group by Customer", "Group by Customer Group", "Group by Transaction", + "Group by Item", "Group by Item Group", "Group by Brand", "Group by Territory", "Group by Sales Person"], + default: "Group by Transaction" + }, + { + fieldname: "group_same_items", + label: __("Group Same Items"), + fieldtype: "Check", + default: 1 + }, { fieldname: "include_taxes", label: __("Include Taxes"), diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py index dfd4bc6fb48d..5c54513bdecc 100644 --- a/erpnext/selling/report/sales_details/sales_details.py +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -3,10 +3,10 @@ from __future__ import unicode_literals import frappe -from frappe import _, scrub -from frappe.utils import getdate, nowdate, flt -from collections import OrderedDict +from frappe import _, scrub, unscrub +from frappe.utils import getdate, nowdate, flt, cint, cstr from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import get_tax_accounts +from frappe.desk.query_report import group_report_data from six import iteritems class SalesPurchaseDetailsReport(object): @@ -30,339 +30,12 @@ def run(self, party_type): self.filters.party_type = party_type - data = self.get_data() - columns = self.get_columns() - return columns, data - - def get_columns(self): - if self.filters.view == "Tree": - columns = [ - { - "label": _("Reference"), - "fieldtype": "Dynamic Link", - "fieldname": "reference", - "options": "doc_type", - "width": 300 - }, - { - "label": _("Type"), - "fieldtype": "Data", - "fieldname": "doc_type", - "width": 110 - }, - { - "label": _("Date"), - "fieldtype": "Date", - "fieldname": "date", - "width": 80 - }, - ] - else: - columns = [ - { - "label": _("Date"), - "fieldtype": "Date", - "fieldname": "date", - "width": 80 - }, - { - "label": _("Voucher No"), - "fieldtype": "Link", - "fieldname": "voucher_no", - "options": self.filters.doctype, - "width": 140 - }, - { - "label": _(self.filters.party_type), - "fieldtype": "Link", - "fieldname": "party", - "options": self.filters.party_type, - "width": 150 - }, - { - "label": _("Item"), - "fieldtype": "Link", - "fieldname": "item_code", - "options": "Item", - "width": 150 - }, - ] - - columns += [ - { - "label": _("UOM"), - "fieldtype": "Link", - "options": "UOM", - "fieldname": "uom", - "width": 50 - }, - { - "label": _("Qty"), - "fieldtype": "Float", - "fieldname": "qty", - "width": 90 - }, - { - "label": _("Net Rate"), - "fieldtype": "Currency", - "fieldname": "base_net_rate", - "options": "Company:company:default_currency", - "width": 120 - }, - { - "label": _("Net Amount"), - "fieldtype": "Currency", - "fieldname": "base_net_amount", - "options": "Company:company:default_currency", - "width": 120 - }, - { - "label": _("Taxes and Charges"), - "fieldtype": "Currency", - "fieldname": "total_tax_amount", - "options": "Company:company:default_currency", - "width": 120 - }, - { - "label": _("Grand Total"), - "fieldtype": "Currency", - "fieldname": "grand_total", - "options": "Company:company:default_currency", - "width": 120 - }, - ] - - if self.filters.include_taxes: - for tax_description in self.tax_columns: - amount_field = "tax_" + scrub(tax_description) - rate_field = amount_field + "_rate" - columns += [ - { - "label": _(tax_description) + " (%)", - "fieldtype": "Percent", - "fieldname": rate_field, - "width": 60 - }, - { - "label": _(tax_description), - "fieldtype": "Currency", - "fieldname": amount_field, - "options": "Company:company:default_currency", - "width": 120 - }, - ] - - if self.filters.party_type == "Customer": - columns += [ - { - "label": _("Sales Person"), - "fieldtype": "Data", - "fieldname": "sales_person", - "width": 150 - }, - { - "label": _("Territory"), - "fieldtype": "Link", - "fieldname": "territory", - "options": "Territory", - "width": 100 - }, - ] - - columns += [ - { - "label": _("Group"), - "fieldtype": "Dynamic Link", - "fieldname": "group", - "options": "group_doctype", - "width": 100 - }, - { - "label": _("Brand"), - "fieldtype": "Link", - "fieldname": "brand", - "options": "Brand", - "width": 100 - }, - ] - - return columns - - def get_data(self): self.get_entries() self.get_itemised_taxes() self.prepare_data() - - data = [] - - if self.filters.view == "Tree": - self.total_row["indent"] = 0 - self.postprocess_row(self.total_row) - - data.append(self.total_row) - for party, docs in iteritems(self.tree): - party_row = self.party_totals[party] - self.postprocess_row(party_row) - party_row["indent"] = 1 - data.append(party_row) - - for docname, items_uoms in iteritems(docs): - doc_row = self.doc_totals[docname] - self.postprocess_row(doc_row) - doc_row["indent"] = 2 - data.append(doc_row) - - for item_code, uom in items_uoms: - item_row = self.doc_item_uom_totals[(docname, item_code, uom)] - self.postprocess_row(item_row) - item_row["indent"] = 3 - data.append(item_row) - else: - for item in self.item_list: - self.postprocess_row(item) - data = self.item_list - - return data - - def prepare_data(self): - # Totals Row Template - total_fields = ['qty', 'base_net_amount', 'base_amount'] - totals_template = {"currency": self.company_currency} - for f in total_fields: - totals_template[f] = 0.0 - for f in self.tax_amount_fields + self.tax_rate_fields: - totals_template[f] = 0.0 - for f in self.tax_rate_fields: - totals_template[f+"_count"] = 0 - - # Containers - self.tree = OrderedDict() - self.item_list = [] - self.party_totals = {} - self.doc_totals = {} - self.doc_item_uom_totals = {} - self.total_row = {"reference": _("'Total'")} - self.total_row.update(totals_template) - - # Build tree and group totals - for d in self.entries: - # Set UOM based on qty field - if self.filters.qty_field == "Transaction Qty": - d.uom = d.uom - elif self.filters.qty_field == "Contents Qty": - d.uom = d.alt_uom or d.stock_uom - else: - d.uom = d.stock_uom - - if self.filters.view == "Tree": - # Add tree nodes if not already there - self.tree.setdefault(d.party, OrderedDict())\ - .setdefault(d.parent, set())\ - .add((d.item_code, d.uom)) - - # Party total row - if d.party not in self.party_totals: - party_row = self.party_totals[d.party] = totals_template.copy() - party_row.update({ - "doc_type": self.filters.party_type, - "reference": d.party, - scrub(self.filters.party_type) + "_name": d.party_name, - "party_name": d.party_name, - "group": d.party_group, - "group_doctype": d.party_group_dt - }) - if self.filters.party_type == "Customer": - details = self.additional_customer_info.get(d.party, frappe._dict()) - party_row.update({ - "sales_person": details.sales_person, - "territory": details.territory - }) - else: - party_row = self.party_totals[d.party] - - # Document total row - if d.parent not in self.doc_totals: - doc_row = self.doc_totals[d.parent] = totals_template.copy() - doc_row.update({ - "date": d.date, - "doc_type": self.filters.doctype, - "reference": d.parent - }) - if self.filters.party_type == "Customer": - doc_row.update({ - "sales_person": d.sales_person, - "territory": d.territory - }) - else: - doc_row = self.doc_totals[d.parent] - - # Doc-Item-UOM row - if (d.parent, d.item_code, d.uom) not in self.doc_item_uom_totals: - item_row = self.doc_item_uom_totals[(d.parent, d.item_code, d.uom)] = totals_template.copy() - item_row.update({ - "date": d.date, - "uom": d.uom, - "group": d.item_group, - "group_doctype": "Item Group", - "brand": d.brand - }) - if self.filters.view == "Tree": - item_row.update({ - "doc_type": "Item", - "reference": d.item_code, - "item_name": d.item_name, - }) - else: - item_row.update({ - "voucher_no": d.parent, - "party": d.party, - "party_name": d.party_name, - scrub(self.filters.party_type) + "_name": d.party_name, - "item_code": d.item_code, - "item_name": d.item_name - }) - - if self.filters.party_type == "Customer": - item_row.update({ - "sales_person": d.sales_person, - "territory": d.territory - }) - - if self.filters.view != "Tree": - self.item_list.append(item_row) - - else: - item_row = self.doc_item_uom_totals[(d.parent, d.item_code, d.uom)] - - # Group totals - for f in total_fields: - item_row[f] += d[f] - if self.filters.view == "Tree": - party_row[f] += d[f] - doc_row[f] += d[f] - self.total_row[f] += d[f] - - for f, tax in zip(self.tax_amount_fields, self.tax_columns): - tax_amount = self.itemised_tax.get(d.name, {}).get(tax, {}).get("tax_amount", 0.0) - item_row[f] += tax_amount - if self.filters.view == "Tree": - doc_row[f] += tax_amount - party_row[f] += tax_amount - self.total_row[f] += tax_amount - for f, tax in zip(self.tax_rate_fields, self.tax_columns): - tax_rate = self.itemised_tax.get(d.name, {}).get(tax, {}).get("tax_rate", 0.0) - if tax_rate == "NA": - tax_rate = 0.0 - if tax_rate: - item_row[f] += tax_rate - item_row[f+"_count"] += 1 - if self.filters.view == "Tree": - doc_row[f] += tax_rate - doc_row[f+"_count"] += 1 - party_row[f] += tax_rate - party_row[f+"_count"] += 1 - self.total_row[f] += tax_rate - self.total_row[f+"_count"] += 1 + data = self.get_grouped_data() + columns = self.get_columns() + return columns, data def get_entries(self): party_field = scrub(self.filters.party_type) @@ -420,7 +93,7 @@ def get_entries(self): filter_conditions=filter_conditions ), self.filters, as_dict=1) - if self.filters.party_type == "Customer" and self.filters.view == "Tree": + if self.filters.party_type == "Customer" and "Group by Customer" in [self.filters.group_by_1, self.filters.group_by_2, self.filters.group_by_3]: additional_customer_info = frappe.db.sql(""" select s.customer, GROUP_CONCAT(DISTINCT s.territory SEPARATOR ', ') as territory @@ -458,6 +131,146 @@ def get_itemised_taxes(self): self.itemised_tax, self.tax_columns = {}, [] self.tax_amount_fields, self.tax_rate_fields = [], [] + def prepare_data(self): + for d in self.entries: + # Set UOM based on qty field + if self.filters.qty_field == "Transaction Qty": + d.uom = d.uom + elif self.filters.qty_field == "Contents Qty": + d.uom = d.alt_uom or d.stock_uom + else: + d.uom = d.stock_uom + + # Add additional fields + d.update({ + "doc_type": "Item", + "reference": d.item_code, + "voucher_no": d.parent, + "group_doctype": "Item Group", + "group": d.item_group, + "brand": d.brand, + scrub(self.filters.party_type) + "_name": d.party_name, + }) + + if "Group by Item" in [self.filters.group_by_1, self.filters.group_by_2, self.filters.group_by_3]: + d['doc_type'] = self.filters.doctype + d['reference'] = d.get("voucher_no") + + # Add tax fields + for f, tax in zip(self.tax_amount_fields, self.tax_columns): + tax_amount = self.itemised_tax.get(d.name, {}).get(tax, {}).get("tax_amount", 0.0) + d[f] = flt(tax_amount) + for f, tax in zip(self.tax_rate_fields, self.tax_columns): + tax_rate = self.itemised_tax.get(d.name, {}).get(tax, {}).get("tax_rate", 0.0) + d[f] = flt(tax_rate) + + self.postprocess_row(d) + + def get_grouped_data(self): + data = self.entries + + self.group_by = [None] + for i in range(3): + group_label = self.filters.get("group_by_" + str(i + 1), "").replace("Group by ", "") + + if not group_label or group_label == "Ungrouped": + continue + if group_label in ['Customer', 'Supplier']: + group_field = "party" + elif group_label == "Transaction": + group_field = "voucher_no" + elif group_label == "Item": + group_field = "item_code" + elif group_label in ["Customer Group", "Supplier Group"]: + group_field = "party_group" + else: + group_field = scrub(group_label) + + self.group_by.append(group_field) + + # Group same items + if cint(self.filters.get("group_same_items")): + data = group_report_data(data, ("item_code", "uom", "voucher_no"), calculate_totals=self.calculate_group_totals, + totals_only=True) + + return group_report_data(data, self.group_by, calculate_totals=self.calculate_group_totals) + + def calculate_group_totals(self, data, group_field, group_value, grouped_by): + total_fields = ['qty', 'base_net_amount', 'base_amount'] + total_fields += self.tax_amount_fields + averageif_fields = self.tax_rate_fields + + totals = {} + + # Copy grouped by into total row + for f, g in iteritems(grouped_by): + totals[f] = g + + # Set zeros + for f in total_fields + averageif_fields + [f + "_count" for f in averageif_fields]: + totals[f] = 0 + + # Add totals + for d in data: + for f in total_fields: + totals[f] += flt(d[f]) + + for f in averageif_fields: + if flt(d[f]): + totals[f] += flt(d[f]) + totals[f + "_count"] += 1 + + # Set group values + if data: + if group_field == ("item_code", "uom", "voucher_no"): + for f, v in iteritems(data[0]): + if f not in totals: + totals[f] = v + + if 'voucher_no' in grouped_by: + fields_to_copy = ['date', 'sales_person', 'territory'] + for f in fields_to_copy: + if f in data[0]: + totals[f] = data[0][f] + totals['date'] = data[0].get('date') + + if 'item_code' in grouped_by: + totals['group_doctype'] = "Item Group" + totals['group'] = data[0].get('item_group') + + if group_field == 'party': + totals['group_doctype'] = data[0].get("party_group_dt") + totals['group'] = data[0].get("party_group") + + if self.filters.party_type == "Customer": + details = self.additional_customer_info.get(group_value, frappe._dict()) + totals.update({ + "sales_person": grouped_by.get("sales_person") or details.sales_person, + "territory": grouped_by.get("territory") or details.territory + }) + + # Set reference field + group_reference_doctypes = { + "party": self.filters.party_type, + "voucher_no": self.filters.doctype, + "item_code": "Item", + } + + if group_field == ("item_code", "uom", "voucher_no") and data: + totals['doc_type'] = data[0].get('doc_type') + totals['reference'] = data[0].get('reference') + else: + reference_field = group_field[0] if isinstance(group_field, (list, tuple)) else group_field + reference_dt = group_reference_doctypes.get(reference_field, unscrub(cstr(reference_field))) + totals['doc_type'] = reference_dt + totals['reference'] = grouped_by.get(reference_field) if group_field else "'Total'" + + if not group_field and self.group_by == [None]: + totals['voucher_no'] = "'Total'" + + self.postprocess_row(totals) + return totals + def postprocess_row(self, row): # Calculate rate rate_fields = [ @@ -478,10 +291,11 @@ def postprocess_row(self, row): # Calculate tax rates by averaging for f in self.tax_rate_fields: row[f] = row.get(f, 0.0) - if row[f + "_count"]: - row[f] /= row[f + "_count"] + if flt(row.get(f + "_count")): + row[f] /= flt(row.get(f + "_count")) - del row[f + "_count"] + if f + "_count" in row: + del row[f + "_count"] def get_qty_fieldname(self): filter_to_field = { @@ -533,5 +347,181 @@ def get_conditions(self): return "and {}".format(" and ".join(conditions)) if conditions else "" + def get_columns(self): + if len(self.group_by) > 1: + columns = [ + { + "label": _("Reference"), + "fieldtype": "Dynamic Link", + "fieldname": "reference", + "options": "doc_type", + "width": 300 + }, + { + "label": _("Type"), + "fieldtype": "Data", + "fieldname": "doc_type", + "width": 110 + }, + ] + + group_list = [self.filters.group_by_1, self.filters.group_by_2, self.filters.group_by_3] + if "Group by Transaction" not in group_list and "Group by Item" not in group_list: + columns.append({ + "label": _(self.filters.doctype), + "fieldtype": "Link", + "fieldname": "voucher_no", + "options": self.filters.doctype, + "width": 140 + }) + + if "Group by Customer" not in group_list and "Group by Supplier" not in group_list: + columns.append({ + "label": _(self.filters.party_type), + "fieldtype": "Link", + "fieldname": "party", + "options": self.filters.party_type, + "width": 150 + }) + + columns += [ + { + "label": _("Date"), + "fieldtype": "Date", + "fieldname": "date", + "width": 80 + }, + ] + else: + columns = [ + { + "label": _("Date"), + "fieldtype": "Date", + "fieldname": "date", + "width": 80 + }, + { + "label": _(self.filters.doctype), + "fieldtype": "Link", + "fieldname": "voucher_no", + "options": self.filters.doctype, + "width": 140 + }, + { + "label": _(self.filters.party_type), + "fieldtype": "Link", + "fieldname": "party", + "options": self.filters.party_type, + "width": 150 + }, + { + "label": _("Item"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 150 + }, + ] + + columns += [ + { + "label": _("UOM"), + "fieldtype": "Link", + "options": "UOM", + "fieldname": "uom", + "width": 50 + }, + { + "label": _("Qty"), + "fieldtype": "Float", + "fieldname": "qty", + "width": 90 + }, + { + "label": _("Net Rate"), + "fieldtype": "Currency", + "fieldname": "base_net_rate", + "options": "Company:company:default_currency", + "width": 120 + }, + { + "label": _("Net Amount"), + "fieldtype": "Currency", + "fieldname": "base_net_amount", + "options": "Company:company:default_currency", + "width": 120 + }, + { + "label": _("Taxes and Charges"), + "fieldtype": "Currency", + "fieldname": "total_tax_amount", + "options": "Company:company:default_currency", + "width": 120 + }, + { + "label": _("Grand Total"), + "fieldtype": "Currency", + "fieldname": "grand_total", + "options": "Company:company:default_currency", + "width": 120 + }, + ] + + if self.filters.include_taxes: + for tax_description in self.tax_columns: + amount_field = "tax_" + scrub(tax_description) + rate_field = amount_field + "_rate" + columns += [ + { + "label": _(tax_description) + " (%)", + "fieldtype": "Percent", + "fieldname": rate_field, + "width": 60 + }, + { + "label": _(tax_description), + "fieldtype": "Currency", + "fieldname": amount_field, + "options": "Company:company:default_currency", + "width": 120 + }, + ] + + if self.filters.party_type == "Customer": + columns += [ + { + "label": _("Sales Person"), + "fieldtype": "Data", + "fieldname": "sales_person", + "width": 150 + }, + { + "label": _("Territory"), + "fieldtype": "Link", + "fieldname": "territory", + "options": "Territory", + "width": 100 + }, + ] + + columns += [ + { + "label": _("Group"), + "fieldtype": "Dynamic Link", + "fieldname": "group", + "options": "group_doctype", + "width": 100 + }, + { + "label": _("Brand"), + "fieldtype": "Link", + "fieldname": "brand", + "options": "Brand", + "width": 100 + }, + ] + + return columns + def execute(filters=None): return SalesPurchaseDetailsReport(filters).run("Customer") From 1bb4cd3eacbe3a419427afa991a4bd7edb77654d Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Fri, 24 May 2019 17:41:11 +0500 Subject: [PATCH 13/13] fix(Sales/Purchase Details): Do not group if not necessary --- erpnext/selling/report/sales_details/sales_details.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/sales_details/sales_details.py b/erpnext/selling/report/sales_details/sales_details.py index 5c54513bdecc..bc5b50e0fdfa 100644 --- a/erpnext/selling/report/sales_details/sales_details.py +++ b/erpnext/selling/report/sales_details/sales_details.py @@ -193,6 +193,9 @@ def get_grouped_data(self): data = group_report_data(data, ("item_code", "uom", "voucher_no"), calculate_totals=self.calculate_group_totals, totals_only=True) + if len(self.group_by) <= 1: + return data + return group_report_data(data, self.group_by, calculate_totals=self.calculate_group_totals) def calculate_group_totals(self, data, group_field, group_value, grouped_by): @@ -435,7 +438,7 @@ def get_columns(self): "label": _("Qty"), "fieldtype": "Float", "fieldname": "qty", - "width": 90 + "width": 80 }, { "label": _("Net Rate"),