mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-09 15:29:30 +00:00
fix(stock): distribute additional costs when incoming items have no value (#58842)
* fix(stock): distribute additional costs when incoming items have no value when every incoming row has zero basic amount, for example a raw material purchased at zero rate, distribute_additional_costs returned early and left additional_cost at 0 on the finished item. the charges were never capitalised into valuation and the balancing debit stayed in stock adjustment instead of reaching stock in hand. the gl composer had its own quantity fallback for the same case, but it divided by the qty of every row rather than the incoming ones, so a manufacture entry booked only a fraction of the cost to the expense account, and it apportioned by qty while valuation apportions by stock qty, which split rows of differing conversion factors two different ways. both sides now take the rows, the basis and its total from a single get_additional_cost_allocation, falling back to transfer_qty so valuation and gl cannot disagree. the basis is unchanged whenever the incoming rows carry value. * test(stock): cover additional cost distribution for zero valued items adds qty based distribution cases for manufacture and material receipt, a manufacture entry asserting the whole cost reaches its expense account, and a conversion factor case asserting the gl split matches the valuation split. updates test_total_basic_amount_zero, which asserted the cost landing in stock adjustment rather than being capitalised.
This commit is contained in:
committed by
GitHub
parent
79fc039092
commit
1728d1b0f5
@@ -26,17 +26,10 @@ class StockEntryGLComposer(BaseStockGLComposer):
|
||||
doc = self.doc
|
||||
gl_entries = super().compose(inventory_account_map)
|
||||
|
||||
if doc.purpose in ("Repack", "Manufacture"):
|
||||
total_basic_amount = sum(flt(t.basic_amount) for t in doc.get("items") if t.is_finished_item)
|
||||
else:
|
||||
total_basic_amount = sum(flt(t.basic_amount) for t in doc.get("items") if t.t_warehouse)
|
||||
|
||||
divide_based_on = total_basic_amount
|
||||
if doc.get("additional_costs") and not total_basic_amount:
|
||||
divide_based_on = sum(item.qty for item in doc.get("items"))
|
||||
incoming_items, basis, divide_based_on = doc.get_additional_cost_allocation()
|
||||
|
||||
item_account_wise_additional_cost = self._build_additional_cost_per_item_account(
|
||||
total_basic_amount, divide_based_on
|
||||
incoming_items, basis, divide_based_on
|
||||
)
|
||||
if item_account_wise_additional_cost:
|
||||
self._append_additional_cost_gl_entries(gl_entries, item_account_wise_additional_cost)
|
||||
@@ -183,24 +176,20 @@ class StockEntryGLComposer(BaseStockGLComposer):
|
||||
)
|
||||
|
||||
def _build_additional_cost_per_item_account(
|
||||
self, total_basic_amount: float, divide_based_on: float
|
||||
self, incoming_items: list, basis: str, divide_based_on: float
|
||||
) -> dict:
|
||||
doc = self.doc
|
||||
item_account_wise_additional_cost = {}
|
||||
if not divide_based_on:
|
||||
return item_account_wise_additional_cost
|
||||
|
||||
for t in doc.get("additional_costs"):
|
||||
for d in doc.get("items"):
|
||||
if doc.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
|
||||
continue
|
||||
elif not d.t_warehouse:
|
||||
continue
|
||||
|
||||
for t in self.doc.get("additional_costs"):
|
||||
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))
|
||||
entry = item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]
|
||||
entry["amount"] += flt(t.amount * multiply_based_on) / divide_based_on
|
||||
entry["base_amount"] += flt(t.base_amount * multiply_based_on) / divide_based_on
|
||||
|
||||
@@ -920,22 +920,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"):
|
||||
|
||||
@@ -2233,10 +2233,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):
|
||||
@@ -2282,6 +2284,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",
|
||||
|
||||
Reference in New Issue
Block a user