fix(gross-profit): handle returns outside sale period

(cherry picked from commit 67d8223f73)
This commit is contained in:
Navin-S-R
2026-01-23 12:40:38 +05:30
committed by Mergify
parent 3e4bd3040a
commit 303dac262c

View File

@@ -5,15 +5,16 @@ from collections import OrderedDict
import frappe import frappe
from frappe import _, qb, scrub from frappe import _, qb, scrub
from frappe.query_builder import Order from frappe.query_builder import Case, Order
from frappe.query_builder.functions import Coalesce
from frappe.utils import cint, flt, formatdate from frappe.utils import cint, flt, formatdate
from pypika.terms import ExistsCriterion
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
get_dimension_with_children, get_dimension_with_children,
) )
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
from erpnext.controllers.queries import get_match_cond
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@@ -851,129 +852,171 @@ class GrossProfitGenerator:
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
def load_invoice_items(self): def load_invoice_items(self):
conditions = "" self.si_list = []
if self.filters.company:
conditions += " and `tabSales Invoice`.company = %(company)s" SalesInvoice = frappe.qb.DocType("Sales Invoice")
if self.filters.from_date: base_query = self.prepare_invoice_query()
conditions += " and posting_date >= %(from_date)s"
if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s"
if self.filters.include_returned_invoices: if self.filters.include_returned_invoices:
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))" invoice_query = base_query.where(
(SalesInvoice.is_return == 0)
| ((SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnull())
)
else: else:
conditions += " and is_return = 0" invoice_query = base_query.where(SalesInvoice.is_return == 0)
if self.filters.item_group: self.si_list += invoice_query.run(as_dict=True)
conditions += f" and {get_item_group_condition(self.filters.item_group)}" self.prepare_vouchers_to_ignore()
if self.filters.sales_person: ret_invoice_query = base_query.where(
conditions += """ (SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnotnull()
and exists(select 1 )
from `tabSales Team` st if self.vouchers_to_ignore:
where st.parent = `tabSales Invoice`.name ret_invoice_query = base_query.where(SalesInvoice.return_against.notin(self.vouchers_to_ignore))
and st.sales_person = %(sales_person)s)
""" self.si_list += ret_invoice_query.run(as_dict=True)
def prepare_invoice_query(self):
SalesInvoice = frappe.qb.DocType("Sales Invoice")
SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item")
Item = frappe.qb.DocType("Item")
SalesTeam = frappe.qb.DocType("Sales Team")
PaymentSchedule = frappe.qb.DocType("Payment Schedule")
query = (
frappe.qb.from_(SalesInvoice)
.join(SalesInvoiceItem)
.on(SalesInvoiceItem.parent == SalesInvoice.name)
.join(Item)
.on(Item.name == SalesInvoiceItem.item_code)
.where((SalesInvoice.docstatus == 1) & (SalesInvoice.is_opening != "Yes"))
)
query = self.apply_common_filters(query, SalesInvoice, SalesInvoiceItem, SalesTeam)
query = query.select(
SalesInvoiceItem.parenttype,
SalesInvoiceItem.parent,
SalesInvoice.posting_date,
SalesInvoice.posting_time,
SalesInvoice.project,
SalesInvoice.update_stock,
SalesInvoice.customer,
SalesInvoice.customer_group,
SalesInvoice.customer_name,
SalesInvoice.territory,
SalesInvoiceItem.item_code,
SalesInvoice.base_net_total.as_("invoice_base_net_total"),
SalesInvoiceItem.item_name,
SalesInvoiceItem.description,
SalesInvoiceItem.warehouse,
SalesInvoiceItem.item_group,
SalesInvoiceItem.brand,
SalesInvoiceItem.so_detail,
SalesInvoiceItem.sales_order,
SalesInvoiceItem.dn_detail,
SalesInvoiceItem.delivery_note,
SalesInvoiceItem.stock_qty.as_("qty"),
SalesInvoiceItem.base_net_rate,
SalesInvoiceItem.base_net_amount,
SalesInvoiceItem.name.as_("item_row"),
SalesInvoice.is_return,
SalesInvoiceItem.cost_center,
SalesInvoiceItem.serial_and_batch_bundle,
)
if self.filters.group_by == "Sales Person": if self.filters.group_by == "Sales Person":
sales_person_cols = """, sales.sales_person, query = query.select(
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount, SalesTeam.sales_person,
sales.incentives (SalesTeam.allocated_percentage * SalesInvoiceItem.base_net_amount / 100).as_(
""" "allocated_amount"
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" ),
else: SalesTeam.incentives,
sales_person_cols = "" )
sales_team_table = ""
query = query.left_join(SalesTeam).on(SalesTeam.parent == SalesInvoice.name)
if self.filters.group_by == "Payment Term": if self.filters.group_by == "Payment Term":
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1, query = query.select(
'{}', Case()
coalesce(schedule.payment_term, '{}')) as payment_term, .when(SalesInvoice.is_return == 1, _("Sales Return"))
schedule.invoice_portion, .else_(Coalesce(PaymentSchedule.payment_term, _("No Terms")))
schedule.payment_amount """.format(_("Sales Return"), _("No Terms")) .as_("payment_term"),
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and PaymentSchedule.invoice_portion,
`tabSales Invoice`.is_return = 0 """ PaymentSchedule.payment_amount,
else: )
payment_term_cols = ""
payment_term_table = ""
if self.filters.get("sales_invoice"): query = query.left_join(PaymentSchedule).on(
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s" (PaymentSchedule.parent == SalesInvoice.name) & (SalesInvoice.is_return == 0)
)
if self.filters.get("item_code"): query = query.orderby(SalesInvoice.posting_date, order=Order.desc).orderby(
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" SalesInvoice.posting_time, order=Order.desc
)
if self.filters.get("cost_center"): return query
def apply_common_filters(self, query, SalesInvoice, SalesInvoiceItem, SalesTeam):
if self.filters.company:
query = query.where(SalesInvoice.company == self.filters.company)
if self.filters.from_date:
query = query.where(SalesInvoice.posting_date >= self.filters.from_date)
if self.filters.to_date:
query = query.where(SalesInvoice.posting_date <= self.filters.to_date)
if self.filters.item_group:
query = query.where(get_item_group_condition(self.filters.item_group))
if self.filters.sales_person:
query = query.where(
ExistsCriterion(
frappe.qb.from_(SalesTeam)
.select(1)
.where(
(SalesTeam.parent == SalesInvoice.name)
& (SalesTeam.sales_person == self.filters.sales_person)
)
)
)
if self.filters.sales_invoice:
query = query.where(SalesInvoice.name == self.filters.sales_invoice)
if self.filters.item_code:
query = query.where(SalesInvoiceItem.item_code == self.filters.item_code)
if self.filters.cost_center:
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center")) self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center) self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
conditions += " and `tabSales Invoice Item`.cost_center in %(cost_center)s" query = query.where(SalesInvoiceItem.cost_center.isin(self.filters.cost_center))
if self.filters.get("project"): if self.filters.project:
self.filters.project = frappe.parse_json(self.filters.get("project")) self.filters.project = frappe.parse_json(self.filters.get("project"))
conditions += " and `tabSales Invoice Item`.project in %(project)s" query = query.where(SalesInvoiceItem.project.isin(self.filters.project))
accounting_dimensions = get_accounting_dimensions(as_list=False) for dim in get_accounting_dimensions(as_list=False) or []:
if accounting_dimensions: if self.filters.get(dim.fieldname):
for dimension in accounting_dimensions: if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
if self.filters.get(dimension.fieldname): self.filters[dim.fieldname] = get_dimension_with_children(
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): dim.document_type, self.filters.get(dim.fieldname)
self.filters[dimension.fieldname] = get_dimension_with_children( )
dimension.document_type, self.filters.get(dimension.fieldname) query = query.where(SalesInvoiceItem[dim.fieldname].isin(self.filters[dim.fieldname]))
)
conditions += (
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
)
else:
conditions += (
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
)
if self.filters.get("warehouse"): if self.filters.warehouse:
warehouse_details = frappe.db.get_value( lft, rgt = frappe.db.get_value("Warehouse", self.filters.warehouse, ["lft", "rgt"])
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1 WH = frappe.qb.DocType("Warehouse")
query = query.where(
SalesInvoiceItem.warehouse.isin(
frappe.qb.from_(WH).select(WH.name).where((WH.lft >= lft) & (WH.rgt <= rgt))
)
) )
if warehouse_details:
conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
self.si_list = frappe.db.sql( return query
"""
select def prepare_vouchers_to_ignore(self):
`tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, self.vouchers_to_ignore = tuple(row["parent"] for row in self.si_list)
`tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time,
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer_name,
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
`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`.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,
`tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.serial_and_batch_bundle
{sales_person_cols}
{payment_term_cols}
from
`tabSales Invoice` inner join `tabSales Invoice Item`
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
join `tabItem` item on item.name = `tabSales Invoice Item`.item_code
{sales_team_table}
{payment_term_table}
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,
payment_term_cols=payment_term_cols,
payment_term_table=payment_term_table,
match_cond=get_match_cond("Sales Invoice"),
),
self.filters,
as_dict=1,
)
def get_delivery_notes(self): def get_delivery_notes(self):
self.delivery_notes = frappe._dict({}) self.delivery_notes = frappe._dict({})