diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index d13492254f8c..112f3cb3c057 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -396,7 +396,7 @@ def test_repost_item_valuation_for_closing_stock_balance(self): doc.from_date = today() doc.to_date = today() doc.submit() - + doc.create_stock_closing_balance_entries() doc.load_from_db() self.assertEqual(doc.docstatus, 1) self.assertEqual(doc.status, "Completed") diff --git a/erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json b/erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json index 57739e0ab084..c524ebe7f280 100644 --- a/erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json +++ b/erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json @@ -29,7 +29,9 @@ "item_group", "column_break_ljle", "stock_uom", - "inventory_dimension_key" + "inventory_dimension_key", + "stock_ageing_section", + "fifo_queue" ], "fields": [ { @@ -216,6 +218,17 @@ "options": "Batch", "read_only": 1, "search_index": 1 + }, + { + "fieldname": "stock_ageing_section", + "fieldtype": "Section Break", + "label": "Stock Ageing" + }, + { + "fieldname": "fifo_queue", + "fieldtype": "Long Text", + "label": "FIFO Queue", + "read_only": 1 } ], "hide_toolbar": 1, @@ -223,7 +236,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-12-10 11:54:58.522295", + "modified": "2024-12-10 21:56:36.633567", "modified_by": "Administrator", "module": "Stock", "name": "Stock Closing Balance", diff --git a/erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.py b/erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.py index d2b26a32e61a..7b419d0382c6 100644 --- a/erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.py +++ b/erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.py @@ -17,6 +17,7 @@ class StockClosingBalance(Document): actual_qty: DF.Float batch_no: DF.Link | None company: DF.Link | None + fifo_queue: DF.LongText | None inventory_dimension_key: DF.SmallText | None item_code: DF.Link | None item_group: DF.Link | None diff --git a/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py b/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py index f3217fecc11f..84a28292000c 100644 --- a/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py +++ b/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py @@ -8,7 +8,7 @@ from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file from frappe.desk.form.load import get_attachments from frappe.model.document import Document -from frappe.utils import add_days, get_link_to_form, nowtime, parse_json +from frappe.utils import add_days, get_date_str, get_link_to_form, nowtime, parse_json from frappe.utils.background_jobs import enqueue from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions @@ -111,13 +111,15 @@ def create_stock_closing_balance_entries(self): stk_cl_obj = StockClosing(self.company, self.from_date, self.to_date) entries = stk_cl_obj.get_stock_closing_entries() - for key in entries: row = entries[key] if row.actual_qty == 0.0 and row.stock_value_difference == 0.0: continue + if row.fifo_queue is not None: + row.fifo_queue = json.dumps(row.fifo_queue) + new_doc = frappe.new_doc("Stock Closing Balance") new_doc.update(row) new_doc.posting_date = self.to_date @@ -143,7 +145,6 @@ def get_prepared_data(self): def prepare_closing_stock_balance(name): doc = frappe.get_doc("Stock Closing Entry", name) - doc.db_set("status", "In Progress") try: @@ -174,38 +175,64 @@ def get_stock_closing_entries(self): key = dimension_values if key in closing_stock: - closing_stock[key].actual_qty += row.sabb_qty or row.actual_qty + actual_qty = row.sabb_qty or row.actual_qty + closing_stock[key].actual_qty += actual_qty closing_stock[key].stock_value_difference += ( row.sabb_stock_value_difference or row.stock_value_difference ) if not row.actual_qty and row.qty_after_transaction: closing_stock[key].actual_qty = row.qty_after_transaction + + fifo_queue = closing_stock[key].fifo_queue + if fifo_queue: + self.update_fifo_queue(fifo_queue, actual_qty, row.posting_date) + closing_stock[key].fifo_queue = fifo_queue else: entries = self.get_initialized_entry(row, dimension_fields) closing_stock[key] = entries return closing_stock + def update_fifo_queue(self, fifo_queue, actual_qty, posting_date): + if actual_qty > 0: + fifo_queue.append([actual_qty, get_date_str(posting_date)]) + else: + remaining_qty = actual_qty + for idx, queue in enumerate(fifo_queue): + if queue[0] + remaining_qty >= 0: + queue[0] += remaining_qty + if queue[0] == 0: + fifo_queue.pop(idx) + break + else: + remaining_qty += queue[0] + fifo_queue.pop(0) + def get_initialized_entry(self, row, dimension_fields): item_details = frappe.get_cached_value( - "Item", row.item_code, ["item_group", "item_name", "stock_uom"], as_dict=1 + "Item", row.item_code, ["item_group", "item_name", "stock_uom", "has_serial_no"], as_dict=1 ) inventory_dimension_key = None if dimension_fields not in [("item_code", "warehouse"), ("item_code", "warehouse", "batch_no")]: inventory_dimension_key = json.dumps(dimension_fields) + actual_qty = row.sabb_qty or row.actual_qty or row.qty_after_transaction + entry = frappe._dict( { "item_code": row.item_code, "warehouse": row.warehouse, - "actual_qty": row.sabb_qty or row.actual_qty or row.qty_after_transaction, + "actual_qty": actual_qty, "stock_value_difference": row.sabb_stock_value_difference or row.stock_value_difference, "item_group": item_details.item_group, "item_name": item_details.item_name, "stock_uom": item_details.stock_uom, "inventory_dimension_key": inventory_dimension_key, + "fifo_queue": [[actual_qty, get_date_str(row.posting_date)]] + if not item_details.has_serial_no + else [], } ) @@ -224,7 +251,7 @@ def get_sle_entries(self): if self.last_closing_balance: self.from_date = add_days(self.last_closing_balance.to_date, 1) sl_entries += self.get_entries( - "Stock Closing Voucher", + "Stock Closing Balance", fields=[ "item_code", "warehouse", @@ -377,7 +404,12 @@ def get_stock_closing_balance(self, kwargs, for_batch=False): query = frappe.qb.from_(table).select("*").where(table.stock_closing_entry == stock_closing_entry) for key, value in kwargs.items(): - if isinstance(value, list) or isinstance(value, tuple): + if key == "inventory_dimension_key": + if isinstance(value, tuple) and value[0] == "is" and value[1] == "not set": + query = query.where( + table.inventory_dimension_key.isnull() | (table.inventory_dimension_key == "") + ) + elif isinstance(value, list) or isinstance(value, tuple): query = query.where(table[key].isin(value)) else: query = query.where(table[key] == value) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index af2fc8dea6fc..eb8057a3655b 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -93,7 +93,7 @@ def prepare_opening_stock(self) -> None: "item_name": entry.item_name, "opening_qty": entry.actual_qty, "opening_val": entry.stock_value_difference, - "opening_fifo_queue": [], + "opening_fifo_queue": json.loads(entry.fifo_queue) if entry.fifo_queue else [], "in_qty": 0.0, "in_val": 0.0, "out_qty": 0.0, @@ -124,6 +124,8 @@ def get_entries_from_stock_closing_balance(self) -> list: if dimenion_keys: query_filters["inventory_dimension_key"] = json.dumps(("item_code", "warehouse", *dimenion_keys)) + else: + query_filters["inventory_dimension_key"] = ("is", "not set") opening_entries = stk_cl_obj.get_stock_closing_balance(query_filters) if not opening_entries: @@ -210,7 +212,7 @@ def prepare_item_warehouse_map_for_current_period(self): def prepare_new_data(self): if self.filters.get("show_stock_ageing_data"): self.filters["show_warehouse_wise_stock"] = True - item_wise_fifo_queue = FIFOSlots(self.filters, self.sle_entries).generate() + item_wise_fifo_queue = FIFOSlots(self.filters).generate() _func = itemgetter(1) @@ -237,6 +239,7 @@ def prepare_new_data(self): opening_fifo_queue.extend(fifo_queue) stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} + if opening_fifo_queue: fifo_queue = sorted(filter(_func, opening_fifo_queue), key=_func) if not fifo_queue: