Merge pull request #58887 from aerele/fix/additional-cost-zero-valued-items-v16

fix(stock): distribute additional costs when incoming items have no value (backport #58842)
This commit is contained in:
Sudharsanan Ashok
2026-09-08 19:55:54 +05:30
committed by GitHub
2 changed files with 207 additions and 33 deletions

View File

@@ -1775,22 +1775,28 @@ class StockEntry(StockController, SubcontractingInwardController):
self.total_additional_costs = sum(flt(t.base_amount) for t in self.get("additional_costs"))
if self.purpose in ("Repack", "Manufacture"):
incoming_items_cost = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
else:
incoming_items_cost = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
if not incoming_items_cost:
return
incoming_items, basis, total_basis = self.get_additional_cost_allocation()
for d in self.get("items"):
if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
d.additional_cost = 0
continue
elif not d.t_warehouse:
d.additional_cost = 0
continue
d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
d.additional_cost = 0
if not total_basis:
return
for d in incoming_items:
d.additional_cost = (flt(d.get(basis)) / total_basis) * self.total_additional_costs
def get_additional_cost_allocation(self):
if self.purpose in ("Repack", "Manufacture"):
incoming_items = [d for d in self.get("items") if d.is_finished_item]
else:
incoming_items = [d for d in self.get("items") if d.t_warehouse]
total_basic_amount = sum(flt(d.basic_amount) for d in incoming_items)
if total_basic_amount:
return incoming_items, "basic_amount", total_basic_amount
return incoming_items, "transfer_qty", sum(flt(d.transfer_qty) for d in incoming_items)
def update_valuation_rate(self, reset_outgoing_rate=True):
for d in self.get("items"):
@@ -2357,32 +2363,20 @@ class StockEntry(StockController, SubcontractingInwardController):
def get_gl_entries(self, inventory_account_map):
gl_entries = super().get_gl_entries(inventory_account_map)
if self.purpose in ("Repack", "Manufacture"):
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
else:
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
divide_based_on = total_basic_amount
if self.get("additional_costs") and not total_basic_amount:
# if total_basic_amount is 0, distribute additional charges based on qty
divide_based_on = sum(item.qty for item in list(self.get("items")))
incoming_items, basis, divide_based_on = self.get_additional_cost_allocation()
item_account_wise_additional_cost = {}
for t in self.get("additional_costs"):
for d in self.get("items"):
if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
continue
elif not d.t_warehouse:
continue
if not divide_based_on:
continue
for d in incoming_items:
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(
t.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
multiply_based_on = d.basic_amount if total_basic_amount else d.qty
multiply_based_on = flt(d.get(basis))
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += (
flt(t.amount * multiply_based_on) / divide_based_on

View File

@@ -3,7 +3,7 @@
from frappe.permissions import add_user_permission, remove_user_permission
from frappe.utils import add_days, cstr, flt, get_time, getdate, nowtime, today
from frappe.utils import add_days, cstr, flt, get_time, getdate, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.controllers.accounts_controller import InvalidQtyError
@@ -1953,10 +1953,12 @@ class TestStockEntry(ERPNextTestSuite):
se.insert()
se.submit()
self.assertEqual([33.33, 66.67], [flt(d.additional_cost, 2) for d in se.items])
self.check_gl_entries(
"Stock Entry",
se.name,
sorted([["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]]),
sorted([["Stock In Hand - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]]),
)
def test_conversion_factor_change(self):
@@ -2002,6 +2004,184 @@ class TestStockEntry(ERPNextTestSuite):
distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([0.0, 100.0, 0.0], distributed_costs)
def test_additional_cost_distribution_manufacture_zero_valued_items(self):
se = frappe.get_doc(
doctype="Stock Entry",
purpose="Manufacture",
additional_costs=[frappe._dict(base_amount=100)],
items=[
frappe._dict(item_code="RM", basic_amount=0, transfer_qty=10),
frappe._dict(
item_code="FG", basic_amount=0, transfer_qty=5, t_warehouse="X", is_finished_item=1
),
frappe._dict(item_code="scrap", basic_amount=0, transfer_qty=2, t_warehouse="X"),
],
)
se.distribute_additional_costs()
distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([0.0, 100.0, 0.0], distributed_costs)
def test_additional_cost_distribution_zero_valued_items(self):
se = frappe.get_doc(
doctype="Stock Entry",
purpose="Material Receipt",
additional_costs=[frappe._dict(base_amount=100)],
items=[
frappe._dict(item_code="RECEIVED_1", basic_amount=0, transfer_qty=20, t_warehouse="X"),
frappe._dict(item_code="RECEIVED_2", basic_amount=0, transfer_qty=30, t_warehouse="X"),
],
)
se.distribute_additional_costs()
distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([40.0, 60.0], distributed_costs)
def test_additional_cost_gl_for_zero_valued_manufacture(self):
company = "_Test Company with perpetual inventory"
rm = make_item("_Test Zero Rate RM", {"is_stock_item": 1}).name
fg = make_item("_Test Zero Rate FG", {"is_stock_item": 1}).name
receipt = frappe.get_doc(
{
"doctype": "Stock Entry",
"purpose": "Material Receipt",
"stock_entry_type": "Material Receipt",
"posting_date": nowdate(),
"company": company,
"items": [
{
"item_code": rm,
"qty": 5,
"basic_rate": 0,
"uom": "Nos",
"t_warehouse": "Stores - TCP1",
"allow_zero_valuation_rate": 1,
"cost_center": "Main - TCP1",
}
],
}
)
receipt.insert()
receipt.submit()
se = frappe.get_doc(
{
"doctype": "Stock Entry",
"purpose": "Manufacture",
"stock_entry_type": "Manufacture",
"posting_date": nowdate(),
"company": company,
"items": [
{
"item_code": rm,
"qty": 5,
"uom": "Nos",
"s_warehouse": "Stores - TCP1",
"cost_center": "Main - TCP1",
},
{
"item_code": fg,
"qty": 5,
"uom": "Nos",
"t_warehouse": "Finished Goods - TCP1",
"is_finished_item": 1,
"cost_center": "Main - TCP1",
},
],
"additional_costs": [
{
"expense_account": "Miscellaneous Expenses - TCP1",
"amount": 500,
"description": "freight",
}
],
}
)
se.insert()
se.submit()
self.assertEqual(500.0, se.items[1].additional_cost)
self.check_gl_entries(
"Stock Entry",
se.name,
sorted([["Stock In Hand - TCP1", 500.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 500.0]]),
)
def test_additional_cost_gl_matches_valuation_split(self):
company = "_Test Company with perpetual inventory"
cost_center = "_Test Additional Cost CC - TCP1"
if not frappe.db.exists("Cost Center", cost_center):
frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "_Test Additional Cost CC",
"company": company,
"is_group": 0,
"parent_cost_center": "_Test Company with perpetual inventory - TCP1",
}
).insert()
uoms = [{"uom": "Nos", "conversion_factor": 1}, {"uom": "Box", "conversion_factor": 2}]
item_a = make_item("_Test Addl Cost CF A", {"is_stock_item": 1, "uoms": uoms}).name
uoms[1]["conversion_factor"] = 3
item_b = make_item("_Test Addl Cost CF B", {"is_stock_item": 1, "uoms": uoms}).name
se = frappe.get_doc(
{
"doctype": "Stock Entry",
"purpose": "Material Receipt",
"stock_entry_type": "Material Receipt",
"posting_date": nowdate(),
"company": company,
"items": [
{
"item_code": item_a,
"qty": 1,
"basic_rate": 0,
"uom": "Box",
"conversion_factor": 2,
"t_warehouse": "Stores - TCP1",
"allow_zero_valuation_rate": 1,
"cost_center": "Main - TCP1",
},
{
"item_code": item_b,
"qty": 1,
"basic_rate": 0,
"uom": "Box",
"conversion_factor": 3,
"t_warehouse": "Stores - TCP1",
"allow_zero_valuation_rate": 1,
"cost_center": cost_center,
},
],
"additional_costs": [
{
"expense_account": "Miscellaneous Expenses - TCP1",
"amount": 100,
"description": "misc",
}
],
}
)
se.insert()
se.submit()
self.assertEqual([40.0, 60.0], [flt(d.additional_cost, 2) for d in se.items])
expense_by_cost_center = frappe.get_all(
"GL Entry",
filters={"voucher_no": se.name, "account": "Miscellaneous Expenses - TCP1"},
fields=["cost_center", "credit"],
)
self.assertEqual(
{"Main - TCP1": 40.0, cost_center: 60.0},
{d.cost_center: d.credit for d in expense_by_cost_center},
)
def test_additional_cost_distribution_non_manufacture(self):
se = frappe.get_doc(
doctype="Stock Entry",