mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-14 17:33:09 +00:00
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:
@@ -1775,22 +1775,28 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
|
|
||||||
self.total_additional_costs = sum(flt(t.base_amount) for t in self.get("additional_costs"))
|
self.total_additional_costs = sum(flt(t.base_amount) for t in self.get("additional_costs"))
|
||||||
|
|
||||||
if self.purpose in ("Repack", "Manufacture"):
|
incoming_items, basis, total_basis = self.get_additional_cost_allocation()
|
||||||
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
|
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
|
d.additional_cost = 0
|
||||||
d.additional_cost = 0
|
|
||||||
continue
|
if not total_basis:
|
||||||
elif not d.t_warehouse:
|
return
|
||||||
d.additional_cost = 0
|
|
||||||
continue
|
for d in incoming_items:
|
||||||
d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
|
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):
|
def update_valuation_rate(self, reset_outgoing_rate=True):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
@@ -2357,32 +2363,20 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
def get_gl_entries(self, inventory_account_map):
|
def get_gl_entries(self, inventory_account_map):
|
||||||
gl_entries = super().get_gl_entries(inventory_account_map)
|
gl_entries = super().get_gl_entries(inventory_account_map)
|
||||||
|
|
||||||
if self.purpose in ("Repack", "Manufacture"):
|
incoming_items, basis, divide_based_on = self.get_additional_cost_allocation()
|
||||||
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")))
|
|
||||||
|
|
||||||
item_account_wise_additional_cost = {}
|
item_account_wise_additional_cost = {}
|
||||||
|
|
||||||
for t in self.get("additional_costs"):
|
for t in self.get("additional_costs"):
|
||||||
for d in self.get("items"):
|
if not divide_based_on:
|
||||||
if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
|
continue
|
||||||
continue
|
for d in incoming_items:
|
||||||
elif not d.t_warehouse:
|
|
||||||
continue
|
|
||||||
|
|
||||||
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
|
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
|
||||||
item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(
|
item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(
|
||||||
t.expense_account, {"amount": 0.0, "base_amount": 0.0}
|
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"] += (
|
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += (
|
||||||
flt(t.amount * multiply_based_on) / divide_based_on
|
flt(t.amount * multiply_based_on) / divide_based_on
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from frappe.permissions import add_user_permission, remove_user_permission
|
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.accounts.doctype.account.test_account import get_inventory_account
|
||||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||||
@@ -1953,10 +1953,12 @@ class TestStockEntry(ERPNextTestSuite):
|
|||||||
se.insert()
|
se.insert()
|
||||||
se.submit()
|
se.submit()
|
||||||
|
|
||||||
|
self.assertEqual([33.33, 66.67], [flt(d.additional_cost, 2) for d in se.items])
|
||||||
|
|
||||||
self.check_gl_entries(
|
self.check_gl_entries(
|
||||||
"Stock Entry",
|
"Stock Entry",
|
||||||
se.name,
|
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):
|
def test_conversion_factor_change(self):
|
||||||
@@ -2002,6 +2004,184 @@ class TestStockEntry(ERPNextTestSuite):
|
|||||||
distributed_costs = [d.additional_cost for d in se.items]
|
distributed_costs = [d.additional_cost for d in se.items]
|
||||||
self.assertEqual([0.0, 100.0, 0.0], distributed_costs)
|
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):
|
def test_additional_cost_distribution_non_manufacture(self):
|
||||||
se = frappe.get_doc(
|
se = frappe.get_doc(
|
||||||
doctype="Stock Entry",
|
doctype="Stock Entry",
|
||||||
|
|||||||
Reference in New Issue
Block a user