diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e699594f5ba..2dbb940a233 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -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 diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index e5405302690..0273888ece6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -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",