diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index c600226e9ee..e6523de122d 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -510,6 +510,7 @@ class GrossProfitGenerator: self.average_buying_rate = {} self.filters = frappe._dict(filters) self.load_invoice_items() + self.load_drop_ship_buying_rates() self.get_delivery_notes() self.load_product_bundle() @@ -641,7 +642,11 @@ class GrossProfitGenerator: returned_item_row.qty += row.qty returned_item_row.base_amount += row.base_amount - if not row.delivered_by_supplier: + if row.delivered_by_supplier: + buying_amount = self.get_drop_ship_buying_amount(row) + if buying_amount is not None: + row.buying_amount = flt(buying_amount, self.currency_precision) + else: row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) def get_average_rate_based_on_group_by(self): @@ -786,28 +791,12 @@ class GrossProfitGenerator: # IMP NOTE # stock_ledger_entries should already be filtered by item_code and warehouse and # sorted by posting_date desc, posting_time desc - if ( - row.delivered_by_supplier - and row.so_detail - and ( - po_details := frappe.get_all( - "Purchase Order Item", - filters={"sales_order_item": row.so_detail, "docstatus": 1}, - pluck="name", - ) - ) - ): - from frappe.query_builder.functions import Sum + if row.delivered_by_supplier: + buying_amount = self.get_drop_ship_buying_amount(row) + if buying_amount is not None: + return buying_amount - table = frappe.qb.DocType("Purchase Invoice Item") - query = ( - frappe.qb.from_(table) - .select(Sum(table.qty * table.base_net_rate)) - .where((table.po_detail.isin(po_details)) & (table.docstatus == 1)) - ) - return flt(query.run()[0][0]) - - elif item_code in self.non_stock_items and (row.project or row.cost_center): + 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 item_rate = self.get_last_purchase_rate(item_code, row) return flt(row.qty) * item_rate @@ -838,6 +827,49 @@ class GrossProfitGenerator: return flt(row.qty) * self.get_average_buying_rate(row, item_code) + def load_drop_ship_buying_rates(self): + self.drop_ship_buying_rates = {} + sales_order_items = { + row.so_detail for row in self.si_list if row.delivered_by_supplier and row.so_detail + } + if not sales_order_items: + return + + from frappe.query_builder.functions import Sum + + purchase_order_item = frappe.qb.DocType("Purchase Order Item") + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") + buying_amounts = ( + frappe.qb.from_(purchase_order_item) + .left_join(purchase_invoice_item) + .on( + (purchase_invoice_item.po_detail == purchase_order_item.name) + & (purchase_invoice_item.docstatus == 1) + ) + .select( + purchase_order_item.sales_order_item, + Sum(purchase_invoice_item.qty * purchase_invoice_item.base_net_rate).as_("buying_amount"), + Sum(purchase_invoice_item.stock_qty).as_("stock_qty"), + ) + .where( + (purchase_order_item.sales_order_item.isin(sales_order_items)) + & (purchase_order_item.docstatus == 1) + ) + .groupby(purchase_order_item.sales_order_item) + .run(as_dict=True) + ) + + for row in buying_amounts: + self.drop_ship_buying_rates[row.sales_order_item] = ( + flt(row.buying_amount) / flt(row.stock_qty) if flt(row.stock_qty) else 0 + ) + + def get_drop_ship_buying_amount(self, row): + if row.so_detail not in self.drop_ship_buying_rates: + return + + return flt(row.qty) * self.drop_ship_buying_rates[row.so_detail] + def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code): from frappe.query_builder.functions import Avg diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index e632b0a83d1..69b36652842 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -676,19 +676,9 @@ class TestGrossProfit(ERPNextTestSuite): self.assertEqual(total[8], 0.0) # gross profit % def test_drop_ship(self): - from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice - from erpnext.selling.doctype.sales_order.mapper import make_purchase_order, make_sales_invoice - from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order - from erpnext.stock.doctype.item.test_item import make_item + from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice - item = make_item("_Test Drop Ship Item", properties={"is_stock_item": 1, "delivered_by_supplier": 1}) - - so = make_sales_order(item=item.name, qty=10, rate=100) - po = make_purchase_order(so.name, selected_items=[so.items[0]])[0] - po.items[0].rate = 80 - po.supplier = "_Test Supplier" - po.submit() - make_purchase_invoice(po.name).submit() + so = self.create_drop_ship_order() si = make_sales_invoice(so.name).submit() filters = frappe._dict( @@ -700,6 +690,58 @@ class TestGrossProfit(ERPNextTestSuite): self.assertIsNone(data[1].buying_rate) self.assertEqual(data[1]["gross_profit_%"], 20) + def test_drop_ship_partial_billing_and_return(self): + from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice + + so = self.create_drop_ship_order() + first_invoice = make_sales_invoice(so.name) + first_invoice.items[0].qty = 4 + first_invoice.submit() + second_invoice = make_sales_invoice(so.name).submit() + + filters = frappe._dict( + company=first_invoice.company, + from_date=first_invoice.posting_date, + to_date=first_invoice.posting_date, + group_by="Invoice", + ) + _, data = execute(filters=filters) + invoice_rows = { + row.parent_invoice: row + for row in data + if row.parent_invoice in {first_invoice.name, second_invoice.name} and row.indent == 1 + } + self.assertEqual(invoice_rows[first_invoice.name].buying_amount, 320) + self.assertEqual(invoice_rows[second_invoice.name].buying_amount, 480) + + sales_return = make_sales_return(first_invoice.name) + sales_return.items[0].qty = -2 + sales_return.submit() + + _, data = execute(filters=filters) + first_invoice_row = next( + row for row in data if row.parent_invoice == first_invoice.name and row.indent == 1 + ) + self.assertEqual(first_invoice_row.qty, 2) + self.assertEqual(first_invoice_row.buying_amount, 160) + self.assertEqual(first_invoice_row.gross_profit, 40) + + def create_drop_ship_order(self, qty=10, selling_rate=100, buying_rate=80): + from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice + from erpnext.selling.doctype.sales_order.mapper import make_purchase_order + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item("_Test Drop Ship Item", properties={"is_stock_item": 1, "delivered_by_supplier": 1}) + so = make_sales_order(item=item.name, qty=qty, rate=selling_rate) + purchase_order = make_purchase_order(so.name, selected_items=[so.items[0]])[0] + purchase_order.items[0].rate = buying_rate + purchase_order.supplier = "_Test Supplier" + purchase_order.submit() + make_purchase_invoice(purchase_order.name).submit() + + return so + def create_rate_adjustment_debit_note(self, against_invoice, adjustment_rate, item_code=None): """Create a rate adjustment debit note with no stock movement.""" dn = self.create_sales_invoice(qty=1, rate=adjustment_rate, do_not_save=True, do_not_submit=True)