fix(accounts): allocate drop-ship cost by invoice quantity (#58226)

This commit is contained in:
Mihir Kandoi
2026-08-17 18:04:02 +05:30
committed by GitHub
parent bf1ec51680
commit d40679cffe
2 changed files with 108 additions and 34 deletions

View File

@@ -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

View File

@@ -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)