From 26e23d5ad02ecf9de68dc5fadf0eefd664cd0ce2 Mon Sep 17 00:00:00 2001 From: fproldan Date: Fri, 15 Aug 2025 14:19:25 +0000 Subject: [PATCH 1/3] fix: gross profit --- .../report/gross_profit/gross_profit.js | 19 +- .../report/gross_profit/gross_profit.py | 899 ++++++++++++------ 2 files changed, 636 insertions(+), 282 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 856b97d16454..217d4daccd7a 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = { "label": __("Company"), "fieldtype": "Link", "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("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") + "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") + "default": frappe.defaults.get_user_default("year_end_date"), + "reqd": 1 }, { "fieldname":"sales_invoice", @@ -42,9 +44,14 @@ frappe.query_reports["Gross Profit"] = { "parent_field": "parent_invoice", "initial_depth": 3, "formatter": function(value, row, column, data, default_formatter) { + if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) { + column._options = "Sales Invoice"; + } else { + column._options = "Item"; + } value = default_formatter(value, row, column, data); - if (data && data.indent == 0.0) { + if (data && (data.indent == 0.0 || (row[1] && row[1].content == "Total"))) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("

").parent().html(); @@ -52,4 +59,4 @@ frappe.query_reports["Gross Profit"] = { return value; }, -} +} \ No newline at end of file diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 3d5e7c50e771..11a3d15272f7 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -1,49 +1,138 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from collections import OrderedDict import frappe -from frappe import _, scrub +from frappe import _, qb, scrub +from frappe.query_builder import Order from frappe.utils import cint, flt from erpnext.controllers.queries import get_match_cond +from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.utils import get_incoming_rate def execute(filters=None): - if not filters: filters = frappe._dict() - filters.currency = frappe.get_cached_value('Company', filters.company, "default_currency") + if not filters: + filters = frappe._dict() + filters.currency = frappe.get_cached_value("Company", filters.company, "default_currency") gross_profit_data = GrossProfitGenerator(filters) data = [] - group_wise_columns = frappe._dict({ - "invoice": ["parent", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", \ - "warehouse", "qty", "base_rate", "buying_rate", "base_amount", - "buying_amount", "gross_profit", "gross_profit_percent", "project"], - "item_code": ["item_code", "item_name", "brand", "description", "qty", "base_rate", - "buying_rate", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], - "warehouse": ["warehouse", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "brand": ["brand", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "item_group": ["item_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "customer": ["customer", "customer_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "customer_group": ["customer_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "sales_person": ["sales_person", "allocated_amount", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], - "territory": ["territory", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"] - }) + group_wise_columns = frappe._dict( + { + "invoice": [ + "invoice_or_item", + "customer", + "customer_group", + "posting_date", + "item_code", + "item_name", + "item_group", + "brand", + "description", + "warehouse", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + "project", + ], + "item_code": [ + "item_code", + "item_name", + "brand", + "description", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "warehouse": [ + "warehouse", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "brand": [ + "brand", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "item_group": [ + "item_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "customer": [ + "customer", + "customer_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "customer_group": [ + "customer_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "sales_person": [ + "sales_person", + "allocated_amount", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], + "territory": [ + "territory", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + } + ) columns = get_columns(group_wise_columns, filters) - if filters.group_by == 'Invoice': + if filters.group_by == "Invoice": get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data) else: @@ -51,11 +140,14 @@ def execute(filters=None): return columns, data -def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data): + +def get_data_when_grouped_by_invoice( + columns, gross_profit_data, filters, group_wise_columns, data +): column_names = get_column_names() # to display item as Item Code: Item Name - columns[0] = 'Sales Invoice:Link/Item:300' + columns[0] = "Sales Invoice:Link/Item:300" # removing Item Code and Item Name columns del columns[4:6] @@ -70,90 +162,220 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_ data.append(row) + def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data): - for idx, src in enumerate(gross_profit_data.grouped_data): + for src in gross_profit_data.grouped_data: row = [] for col in group_wise_columns.get(scrub(filters.group_by)): row.append(src.get(col)) row.append(filters.currency) - if idx == len(gross_profit_data.grouped_data)-1: - row[0] = frappe.bold("Total") + data.append(row) + def get_columns(group_wise_columns, filters): columns = [] - column_map = frappe._dict({ - "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date:100", - "posting_time": _("Posting Time") + ":Data:100", - "item_code": _("Item Code") + ":Link/Item:100", - "item_name": _("Item Name") + ":Data:100", - "item_group": _("Item Group") + ":Link/Item Group:100", - "brand": _("Brand") + ":Link/Brand:100", - "description": _("Description") +":Data:100", - "warehouse": _("Warehouse") + ":Link/Warehouse:100", - "qty": _("Qty") + ":Float:80", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", - "buying_rate": _("Valuation Rate") + ":Currency/currency:100", - "base_amount": _("Selling Amount") + ":Currency/currency:100", - "buying_amount": _("Buying Amount") + ":Currency/currency:100", - "gross_profit": _("Gross Profit") + ":Currency/currency:100", - "gross_profit_percent": _("Gross Profit %") + ":Percent:100", - "project": _("Project") + ":Link/Project:100", - "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", - "customer": _("Customer") + ":Link/Customer:100", - "customer_group": _("Customer Group") + ":Link/Customer Group:100", - "territory": _("Territory") + ":Link/Territory:100" - }) + column_map = frappe._dict( + { + "parent": { + "label": _("Sales Invoice"), + "fieldname": "parent_invoice", + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, + }, + "invoice_or_item": { + "label": _("Sales Invoice"), + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, + }, + "posting_date": { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + "posting_time": { + "label": _("Posting Time"), + "fieldname": "posting_time", + "fieldtype": "Data", + "width": 100, + }, + "item_code": { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + "item_name": { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 100, + }, + "item_group": { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + "brand": {"label": _("Brand"), "fieldtype": "Link", "options": "Brand", "width": 100}, + "description": { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "width": 100, + }, + "warehouse": { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "warehouse", + "width": 100, + }, + "qty": {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80}, + "base_rate": { + "label": _("Avg. Selling Rate"), + "fieldname": "avg._selling_rate", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "buying_rate": { + "label": _("Valuation Rate"), + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "base_amount": { + "label": _("Selling Amount"), + "fieldname": "selling_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "buying_amount": { + "label": _("Buying Amount"), + "fieldname": "buying_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "gross_profit": { + "label": _("Gross Profit"), + "fieldname": "gross_profit", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "gross_profit_percent": { + "label": _("Gross Profit Percent"), + "fieldname": "gross_profit_%", + "fieldtype": "Percent", + "width": 100, + }, + "project": { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 100, + }, + "sales_person": { + "label": _("Sales Person"), + "fieldname": "sales_person", + "fieldtype": "Data", + "width": 100, + }, + "allocated_amount": { + "label": _("Allocated Amount"), + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "customer": { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 100, + }, + "customer_group": { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "customer", + "width": 100, + }, + "territory": { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "territory", + "width": 100, + }, + } + ) for col in group_wise_columns.get(scrub(filters.group_by)): columns.append(column_map.get(col)) - columns.append({ - "fieldname": "currency", - "label" : _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "hidden": 1 - }) + columns.append( + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1, + } + ) return columns + def get_column_names(): - return frappe._dict({ - 'parent': 'sales_invoice', - 'customer': 'customer', - 'customer_group': 'customer_group', - 'posting_date': 'posting_date', - 'item_code': 'item_code', - 'item_name': 'item_name', - 'item_group': 'item_group', - 'brand': 'brand', - 'description': 'description', - 'warehouse': 'warehouse', - 'qty': 'qty', - 'base_rate': 'avg._selling_rate', - 'buying_rate': 'valuation_rate', - 'base_amount': 'selling_amount', - 'buying_amount': 'buying_amount', - 'gross_profit': 'gross_profit', - 'gross_profit_percent': 'gross_profit_%', - 'project': 'project' - }) + return frappe._dict( + { + "invoice_or_item": "sales_invoice", + "customer": "customer", + "customer_group": "customer_group", + "posting_date": "posting_date", + "item_code": "item_code", + "item_name": "item_name", + "item_group": "item_group", + "brand": "brand", + "description": "description", + "warehouse": "warehouse", + "qty": "qty", + "base_rate": "avg._selling_rate", + "buying_rate": "valuation_rate", + "base_amount": "selling_amount", + "buying_amount": "buying_amount", + "gross_profit": "gross_profit", + "gross_profit_percent": "gross_profit_%", + "project": "project", + } + ) + class GrossProfitGenerator(object): def __init__(self, filters=None): + self.sle = {} self.data = [] self.average_buying_rate = {} self.filters = frappe._dict(filters) self.load_invoice_items() + self.get_delivery_notes() - if filters.group_by == 'Invoice': + if filters.group_by == "Invoice": self.group_items_by_invoice() - self.load_stock_ledger_entries() self.load_product_bundle() self.load_non_stock_items() self.get_returned_invoice_items() @@ -172,7 +394,7 @@ def process(self): buying_amount = 0 for row in reversed(self.si_list): - if self.skip_row(row, self.product_bundles): + if self.skip_row(row): continue row.base_amount = flt(row.base_net_amount, self.currency_precision) @@ -181,17 +403,19 @@ def process(self): if row.update_stock: product_bundles = self.product_bundles.get(row.parenttype, {}).get(row.parent, frappe._dict()) elif row.dn_detail: - product_bundles = self.product_bundles.get("Delivery Note", {})\ - .get(row.delivery_note, frappe._dict()) + product_bundles = self.product_bundles.get("Delivery Note", {}).get( + row.delivery_note, frappe._dict() + ) row.item_row = row.dn_detail # get buying amount if row.item_code in product_bundles: - row.buying_amount = flt(self.get_buying_amount_from_product_bundle(row, - product_bundles[row.item_code]), self.currency_precision) + row.buying_amount = flt( + self.get_buying_amount_from_product_bundle(row, product_bundles[row.item_code]), + self.currency_precision, + ) else: - row.buying_amount = flt(self.get_buying_amount(row, row.item_code), - self.currency_precision) + row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision) if grouped_by_invoice: if row.indent == 1.0: @@ -211,7 +435,9 @@ def process(self): # calculate gross profit row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision) if row.base_amount: - row.gross_profit_percent = flt((row.gross_profit / row.base_amount) * 100.0, self.currency_precision) + row.gross_profit_percent = flt( + (row.gross_profit / row.base_amount) * 100.0, self.currency_precision + ) else: row.gross_profit_percent = 0.0 @@ -222,67 +448,73 @@ def process(self): self.get_average_rate_based_on_group_by() def get_average_rate_based_on_group_by(self): - # sum buying / selling totals for group - self.totals = frappe._dict( - qty=0, - base_amount=0, - buying_amount=0, - gross_profit=0, - gross_profit_percent=0, - base_rate=0, - buying_rate=0 - ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): - if i==0: + if i == 0: new_row = row else: new_row.qty += flt(row.qty) new_row.buying_amount += flt(row.buying_amount, self.currency_precision) new_row.base_amount += flt(row.base_amount, self.currency_precision) + if self.filters.get("group_by") == "Sales Person": + new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) - self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): - if row.parent in self.returned_invoices \ - and row.item_code in self.returned_invoices[row.parent]: - returned_item_rows = self.returned_invoices[row.parent][row.item_code] - for returned_item_row in returned_item_rows: - row.qty += flt(returned_item_row.qty) - row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) - row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) - if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row): - row = self.set_average_rate(row) - self.grouped_data.append(row) - self.add_to_totals(row) - self.set_average_gross_profit(self.totals) - self.grouped_data.append(self.totals) + if row.indent == 1.0: + if ( + row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent] + ): + returned_item_rows = self.returned_invoices[row.parent][row.item_code] + for returned_item_row in returned_item_rows: + # returned_items 'qty' should be stateful + if returned_item_row.qty != 0: + if row.qty >= abs(returned_item_row.qty): + row.qty += returned_item_row.qty + returned_item_row.qty = 0 + else: + row.qty = 0 + returned_item_row.qty += row.qty + row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) + row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) + if flt(row.qty) or row.base_amount: + row = self.set_average_rate(row) + self.grouped_data.append(row) def is_not_invoice_row(self, row): - return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice" + return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get( + "group_by" + ) != "Invoice" def set_average_rate(self, new_row): self.set_average_gross_profit(new_row) - new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + new_row.buying_rate = ( + flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + ) + new_row.base_rate = ( + flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + ) return new_row def set_average_gross_profit(self, new_row): new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) - new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ - if new_row.base_amount else 0 - new_row.buying_rate = flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 - new_row.base_rate = flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 - - def add_to_totals(self, new_row): - for key in self.totals: - if new_row.get(key): - self.totals[key] += new_row[key] + new_row.gross_profit_percent = ( + flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) + if new_row.base_amount + else 0 + ) + new_row.buying_rate = ( + flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 + ) + new_row.base_rate = ( + flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 + ) def get_returned_invoice_items(self): - returned_invoices = frappe.db.sql(""" + returned_invoices = frappe.db.sql( + """ select si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against from @@ -291,103 +523,172 @@ def get_returned_invoice_items(self): si.name = si_item.parent and si.docstatus = 1 and si.is_return = 1 - """, as_dict=1) + """, + as_dict=1, + ) self.returned_invoices = frappe._dict() for inv in returned_invoices: - self.returned_invoices.setdefault(inv.return_against, frappe._dict())\ - .setdefault(inv.item_code, []).append(inv) + self.returned_invoices.setdefault(inv.return_against, frappe._dict()).setdefault( + inv.item_code, [] + ).append(inv) - def skip_row(self, row, product_bundles): + def skip_row(self, row): if self.filters.get("group_by") != "Invoice": if not row.get(scrub(self.filters.get("group_by", ""))): return True - elif row.get("is_return") == 1: - return True + + return False def get_buying_amount_from_product_bundle(self, row, product_bundle): buying_amount = 0.0 for packed_item in product_bundle: - if packed_item.get("parent_detail_docname")==row.item_row: + if packed_item.get("parent_detail_docname") == row.item_row: buying_amount += self.get_buying_amount(row, packed_item.item_code) return flt(buying_amount, self.currency_precision) + def calculate_buying_amount_from_sle(self, row, my_sle, parenttype, parent, item_row, item_code): + for i, sle in enumerate(my_sle): + # find the stock valution rate from stock ledger entry + if ( + sle.voucher_type == parenttype + and parent == sle.voucher_no + and sle.voucher_detail_no == item_row + ): + previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 + + if previous_stock_value: + return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) + else: + return flt(row.qty) * self.get_average_buying_rate(row, item_code) + return 0.0 + def get_buying_amount(self, row, item_code): # IMP NOTE # stock_ledger_entries should already be filtered by item_code and warehouse and # sorted by posting_date desc, posting_time desc if item_code in self.non_stock_items and (row.project or row.cost_center): - #Issue 6089-Get last purchasing rate for non-stock item + # Issue 6089-Get last purchasing rate for non-stock item item_rate = self.get_last_purchase_rate(item_code, row) return flt(row.qty) * item_rate else: - my_sle = self.sle.get((item_code, row.warehouse)) + my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) if (row.update_stock or row.dn_detail) and my_sle: parenttype, parent = row.parenttype, row.parent if row.dn_detail: parenttype, parent = "Delivery Note", row.delivery_note - for i, sle in enumerate(my_sle): - # find the stock valution rate from stock ledger entry - if sle.voucher_type == parenttype and parent == sle.voucher_no and \ - sle.voucher_detail_no == row.item_row: - previous_stock_value = len(my_sle) > i+1 and \ - flt(my_sle[i+1].stock_value) or 0.0 - - if previous_stock_value: - return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) - else: - return flt(row.qty) * self.get_average_buying_rate(row, item_code) + return self.calculate_buying_amount_from_sle( + row, my_sle, parenttype, parent, row.item_row, item_code + ) + elif self.delivery_notes.get((row.parent, row.item_code), None): + # check if Invoice has delivery notes + dn = self.delivery_notes.get((row.parent, row.item_code)) + parenttype, parent, item_row, warehouse = ( + "Delivery Note", + dn["delivery_note"], + dn["item_row"], + dn["warehouse"], + ) + my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) + return self.calculate_buying_amount_from_sle( + row, my_sle, parenttype, parent, item_row, item_code + ) + elif row.sales_order and row.so_detail: + incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code) + if incoming_amount: + return incoming_amount else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) - return 0.0 + return flt(row.qty) * self.get_average_buying_rate(row, item_code) + + def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code): + from frappe.query_builder.functions import Sum + + delivery_note_item = frappe.qb.DocType("Delivery Note Item") + + query = ( + frappe.qb.from_(delivery_note_item) + .select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) + .where(delivery_note_item.docstatus == 1) + .where(delivery_note_item.item_code == item_code) + .where(delivery_note_item.against_sales_order == sales_order) + .where(delivery_note_item.so_detail == so_detail) + .groupby(delivery_note_item.item_code) + ) + + incoming_amount = query.run() + return flt(incoming_amount[0][0]) if incoming_amount else 0 def get_average_buying_rate(self, row, item_code): args = row if not item_code in self.average_buying_rate: - args.update({ - 'voucher_type': row.parenttype, - 'voucher_no': row.parent, - 'allow_zero_valuation': True, - 'company': self.filters.company - }) + args.update( + { + "voucher_type": row.parenttype, + "voucher_no": row.parent, + "allow_zero_valuation": True, + "company": self.filters.company, + } + ) average_buying_rate = get_incoming_rate(args) - self.average_buying_rate[item_code] = flt(average_buying_rate) + self.average_buying_rate[item_code] = flt(average_buying_rate) return self.average_buying_rate[item_code] def get_last_purchase_rate(self, item_code, row): - condition = '' + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") + + query = ( + frappe.qb.from_(purchase_invoice_item) + .inner_join(purchase_invoice) + .on(purchase_invoice.name == purchase_invoice_item.parent) + .select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor) + .where(purchase_invoice.docstatus == 1) + .where(purchase_invoice.posting_date <= self.filters.to_date) + .where(purchase_invoice_item.item_code == item_code) + ) + if row.project: - condition += " AND a.project=%s" % (frappe.db.escape(row.project)) - elif row.cost_center: - condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) - if self.filters.to_date: - condition += " AND modified='%s'" % (self.filters.to_date) + query.where(purchase_invoice_item.project == row.project) - last_purchase_rate = frappe.db.sql(""" - select (a.base_rate / a.conversion_factor) - from `tabPurchase Invoice Item` a - where a.item_code = %s and a.docstatus=1 - {0} - order by a.modified desc limit 1""".format(condition), item_code) + if row.cost_center: + query.where(purchase_invoice_item.cost_center == row.cost_center) + + query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc) + query.limit(1) + last_purchase_rate = query.run() return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 def load_invoice_items(self): conditions = "" if self.filters.company: - conditions += " and company = %(company)s" + conditions += " and `tabSales Invoice`.company = %(company)s" if self.filters.from_date: conditions += " and posting_date >= %(from_date)s" if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" - if self.filters.group_by=="Sales Person": + conditions += " and (is_return = 0 or (is_return=1 and return_against is null))" + + if self.filters.item_group: + conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) + + if self.filters.sales_person: + conditions += """ + and exists(select 1 + from `tabSales Team` st + where st.parent = `tabSales Invoice`.name + and st.sales_person = %(sales_person)s) + """ + + if self.filters.group_by == "Sales Person": sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" else: @@ -400,7 +701,8 @@ def load_invoice_items(self): if self.filters.get("item_code"): conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" - self.si_list = frappe.db.sql(""" + self.si_list = frappe.db.sql( + """ select `tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time, @@ -409,7 +711,8 @@ def load_invoice_items(self): `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, - `tabSales Invoice Item`.brand, `tabSales Invoice Item`.dn_detail, + `tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail, + `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return, @@ -422,137 +725,181 @@ def load_invoice_items(self): where `tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond} order by - `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""" - .format(conditions=conditions, sales_person_cols=sales_person_cols, - sales_team_table=sales_team_table, match_cond = get_match_cond('Sales Invoice')), self.filters, as_dict=1) + `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format( + conditions=conditions, + sales_person_cols=sales_person_cols, + sales_team_table=sales_team_table, + match_cond=get_match_cond("Sales Invoice"), + ), + self.filters, + as_dict=1, + ) + + def get_delivery_notes(self): + self.delivery_notes = frappe._dict({}) + if self.si_list: + invoices = [x.parent for x in self.si_list] + dni = qb.DocType("Delivery Note Item") + delivery_notes = ( + qb.from_(dni) + .select( + dni.against_sales_invoice.as_("sales_invoice"), + dni.item_code, + dni.warehouse, + dni.parent.as_("delivery_note"), + dni.name.as_("item_row"), + ) + .where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices))) + .groupby(dni.against_sales_invoice, dni.item_code) + .orderby(dni.creation, order=Order.desc) + .run(as_dict=True) + ) + + for entry in delivery_notes: + self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry def group_items_by_invoice(self): """ - Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. + Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. """ - parents = [] + grouped = OrderedDict() for row in self.si_list: - if row.parent not in parents: - parents.append(row.parent) + # initialize list with a header row for each new parent + grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append( + row.update( + {"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code} + ) # descendant rows will have indent: 1.0 or greater + ) - parents_index = 0 - for index, row in enumerate(self.si_list): - if parents_index < len(parents) and row.parent == parents[parents_index]: - invoice = self.get_invoice_row(row) - self.si_list.insert(index, invoice) - parents_index += 1 + # if item is a bundle, add it's components as seperate rows + if frappe.db.exists("Product Bundle", row.item_code): + bundled_items = self.get_bundle_items(row) + for x in bundled_items: + bundle_item = self.get_bundle_item_row(row, x) + grouped.get(row.parent).append(bundle_item) - else: - # skipping the bundle items rows - if not row.indent: - row.indent = 1.0 - row.parent_invoice = row.parent - row.parent = row.item_code + self.si_list.clear() - if frappe.db.exists('Product Bundle', row.item_code): - self.add_bundle_items(row, index) + for items in grouped.values(): + self.si_list.extend(items) def get_invoice_row(self, row): - return frappe._dict({ - 'parent_invoice': "", - 'indent': 0.0, - 'parent': row.parent, - 'posting_date': row.posting_date, - 'posting_time': row.posting_time, - 'project': row.project, - 'update_stock': row.update_stock, - 'customer': row.customer, - 'customer_group': row.customer_group, - 'item_code': None, - 'item_name': None, - 'description': None, - 'warehouse': None, - 'item_group': None, - 'brand': None, - 'dn_detail': None, - 'delivery_note': None, - 'qty': None, - 'item_row': None, - 'is_return': row.is_return, - 'cost_center': row.cost_center, - 'base_net_amount': frappe.db.get_value('Sales Invoice', row.parent, 'base_net_total') - }) - - def add_bundle_items(self, product_bundle, index): - bundle_items = self.get_bundle_items(product_bundle) - - for i, item in enumerate(bundle_items): - bundle_item = self.get_bundle_item_row(product_bundle, item) - self.si_list.insert((index+i+1), bundle_item) + # header row format + return frappe._dict( + { + "parent_invoice": "", + "indent": 0.0, + "invoice_or_item": row.parent, + "parent": None, + "posting_date": row.posting_date, + "posting_time": row.posting_time, + "project": row.project, + "update_stock": row.update_stock, + "customer": row.customer, + "customer_group": row.customer_group, + "item_code": None, + "item_name": None, + "description": None, + "warehouse": None, + "item_group": None, + "brand": None, + "dn_detail": None, + "delivery_note": None, + "qty": None, + "item_row": None, + "is_return": row.is_return, + "cost_center": row.cost_center, + "base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"), + } + ) def get_bundle_items(self, product_bundle): return frappe.get_all( - 'Product Bundle Item', - filters = { - 'parent': product_bundle.item_code - }, - fields = ['item_code', 'qty'] + "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] ) def get_bundle_item_row(self, product_bundle, item): item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code) - return frappe._dict({ - 'parent_invoice': product_bundle.item_code, - 'indent': product_bundle.indent + 1, - 'parent': item.item_code, - 'posting_date': product_bundle.posting_date, - 'posting_time': product_bundle.posting_time, - 'project': product_bundle.project, - 'customer': product_bundle.customer, - 'customer_group': product_bundle.customer_group, - 'item_code': item.item_code, - 'item_name': item_name, - 'description': description, - 'warehouse': product_bundle.warehouse, - 'item_group': item_group, - 'brand': brand, - 'dn_detail': product_bundle.dn_detail, - 'delivery_note': product_bundle.delivery_note, - 'qty': (flt(product_bundle.qty) * flt(item.qty)), - 'item_row': None, - 'is_return': product_bundle.is_return, - 'cost_center': product_bundle.cost_center - }) + return frappe._dict( + { + "parent_invoice": product_bundle.item_code, + "indent": product_bundle.indent + 1, + "parent": None, + "invoice_or_item": item.item_code, + "posting_date": product_bundle.posting_date, + "posting_time": product_bundle.posting_time, + "project": product_bundle.project, + "customer": product_bundle.customer, + "customer_group": product_bundle.customer_group, + "item_code": item.item_code, + "item_name": item_name, + "description": description, + "warehouse": product_bundle.warehouse, + "item_group": item_group, + "brand": brand, + "dn_detail": product_bundle.dn_detail, + "delivery_note": product_bundle.delivery_note, + "qty": (flt(product_bundle.qty) * flt(item.qty)), + "item_row": None, + "is_return": product_bundle.is_return, + "cost_center": product_bundle.cost_center, + } + ) def get_bundle_item_details(self, item_code): return frappe.db.get_value( - 'Item', - item_code, - ['item_name', 'description', 'item_group', 'brand'] + "Item", item_code, ["item_name", "description", "item_group", "brand"] ) - def load_stock_ledger_entries(self): - res = frappe.db.sql("""select item_code, voucher_type, voucher_no, - voucher_detail_no, stock_value, warehouse, actual_qty as qty - from `tabStock Ledger Entry` - where company=%(company)s and is_cancelled = 0 - order by - item_code desc, warehouse desc, posting_date desc, - posting_time desc, creation desc""", self.filters, as_dict=True) - self.sle = {} - for r in res: - if (r.item_code, r.warehouse) not in self.sle: - self.sle[(r.item_code, r.warehouse)] = [] - - self.sle[(r.item_code, r.warehouse)].append(r) + def get_stock_ledger_entries(self, item_code, warehouse): + if item_code and warehouse: + if (item_code, warehouse) not in self.sle: + sle = qb.DocType("Stock Ledger Entry") + res = ( + qb.from_(sle) + .select( + sle.item_code, + sle.voucher_type, + sle.voucher_no, + sle.voucher_detail_no, + sle.stock_value, + sle.warehouse, + sle.actual_qty.as_("qty"), + ) + .where( + (sle.company == self.filters.company) + & (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.is_cancelled == 0) + ) + .orderby(sle.item_code) + .orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc) + .run(as_dict=True) + ) + + self.sle[(item_code, warehouse)] = res + + return self.sle[(item_code, warehouse)] + return [] def load_product_bundle(self): self.product_bundles = {} - for d in frappe.db.sql("""select parenttype, parent, parent_item, + for d in frappe.db.sql( + """select parenttype, parent, parent_item, item_code, warehouse, -1*qty as total_qty, parent_detail_docname - from `tabPacked Item` where docstatus=1""", as_dict=True): - self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault(d.parent, - frappe._dict()).setdefault(d.parent_item, []).append(d) + from `tabPacked Item` where docstatus=1""", + as_dict=True, + ): + self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault( + d.parent, frappe._dict() + ).setdefault(d.parent_item, []).append(d) def load_non_stock_items(self): - self.non_stock_items = frappe.db.sql_list("""select name from tabItem - where is_stock_item=0""") + self.non_stock_items = frappe.db.sql_list( + """select name from tabItem + where is_stock_item=0""" + ) \ No newline at end of file From 122383ddbfac01e8af322b15788d45c0ebd3fc45 Mon Sep 17 00:00:00 2001 From: fproldan Date: Fri, 15 Aug 2025 14:25:02 +0000 Subject: [PATCH 2/3] fix --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 11a3d15272f7..12d2bcf20e5c 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -274,7 +274,7 @@ def get_columns(group_wise_columns, filters): "width": 100, }, "gross_profit_percent": { - "label": _("Gross Profit Percent"), + "label": _("Beneficio Bruto %"), "fieldname": "gross_profit_%", "fieldtype": "Percent", "width": 100, From 3b02ab28e5bd2b24d44e95e1c1922b97dae97c33 Mon Sep 17 00:00:00 2001 From: fproldan Date: Mon, 18 Aug 2025 12:55:26 +0000 Subject: [PATCH 3/3] fix: link --- erpnext/accounts/report/gross_profit/gross_profit.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 217d4daccd7a..f952b522ec62 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -46,6 +46,8 @@ frappe.query_reports["Gross Profit"] = { "formatter": function(value, row, column, data, default_formatter) { if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) { column._options = "Sales Invoice"; + } else if (column.fieldname == "project" && column.options == "Project") { + column._options = "Project"; } else { column._options = "Item"; }