mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-18 08:58:43 +00:00
fix(accounts): allocate drop-ship cost by invoice quantity (#58226)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user