From bc001d2d9ac70632f82f0005f22109873892e604 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Mon, 16 Sep 2019 19:57:04 +0530 Subject: [PATCH] feat: Add stock ageing data to stock balance report (#19036) * feat: Add stock ageing data to stock balance report * fix: Use fifo queue warehouse wise * fix: "Stock Ledger Entry" get query * fix: Remove unwanted quotes in item details query * fix: Check if no SLE was passed * fix: Codacy * fix: Add logic to include additional UOM columns * fix: Show stock ageing data optionally --- .../stock/report/stock_ageing/stock_ageing.py | 21 ++--- .../report/stock_balance/stock_balance.js | 11 ++- .../report/stock_balance/stock_balance.py | 83 ++++++++++++------- erpnext/stock/utils.py | 34 +++++++- 4 files changed, 105 insertions(+), 44 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 83a1d7b62b..e83bf1db2f 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -131,19 +131,20 @@ def get_columns(filters): return columns -def get_fifo_queue(filters): +def get_fifo_queue(filters, sle=None): item_details = {} - transfered_item_details = {} + transferred_item_details = {} serial_no_batch_purchase_details = {} - sle = get_stock_ledger_entries(filters) + if sle == None: + sle = get_stock_ledger_entries(filters) for d in sle: - key = (d.name, d.warehouse) if filters.get('show_warehouse_wise_stock') else d.name + key = (d.name, d.warehouse) if filters.get('show_warehouse_wise_stock') else d.name item_details.setdefault(key, {"details": d, "fifo_queue": []}) fifo_queue = item_details[key]["fifo_queue"] - transfered_item_details.setdefault((d.voucher_no, d.name), []) + transferred_item_details.setdefault((d.voucher_no, d.name), []) if d.voucher_type == "Stock Reconciliation": d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0)) @@ -151,10 +152,10 @@ def get_fifo_queue(filters): serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else [] if d.actual_qty > 0: - if transfered_item_details.get((d.voucher_no, d.name)): - batch = transfered_item_details[(d.voucher_no, d.name)][0] + if transferred_item_details.get((d.voucher_no, d.name)): + batch = transferred_item_details[(d.voucher_no, d.name)][0] fifo_queue.append(batch) - transfered_item_details[((d.voucher_no, d.name))].pop(0) + transferred_item_details[((d.voucher_no, d.name))].pop(0) else: if serial_no_list: for serial_no in serial_no_list: @@ -178,11 +179,11 @@ def get_fifo_queue(filters): # if batch qty > 0 # not enough or exactly same qty in current batch, clear batch qty_to_pop -= batch[0] - transfered_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0)) + transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0)) else: # all from current batch batch[0] -= qty_to_pop - transfered_item_details[(d.voucher_no, d.name)].append([qty_to_pop, batch[1]]) + transferred_item_details[(d.voucher_no, d.name)].append([qty_to_pop, batch[1]]) qty_to_pop = 0 item_details[key]["qty_after_transaction"] = d.qty_after_transaction diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 3829d6a5b4..537fa7c04b 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -41,7 +41,7 @@ frappe.query_reports["Stock Balance"] = { "get_query": function() { return { query: "erpnext.controllers.queries.item_query", - } + }; } }, { @@ -57,7 +57,7 @@ frappe.query_reports["Stock Balance"] = { filters: { 'warehouse_type': warehouse_type } - } + }; } } }, @@ -79,5 +79,10 @@ frappe.query_reports["Stock Balance"] = { "label": __("Show Variant Attributes"), "fieldtype": "Check" }, + { + "fieldname": 'show_stock_ageing_data', + "label": __('Show Stock Ageing Data'), + "fieldtype": 'Check' + }, ] -} +}; diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 8b6590d08e..e5ae70c8d4 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -4,10 +4,12 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, cint, getdate, now -from erpnext.stock.utils import update_included_uom_in_report +from frappe.utils import flt, cint, getdate, now, date_diff +from erpnext.stock.utils import add_additional_uom_columns from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition +from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age + from six import iteritems def execute(filters=None): @@ -15,11 +17,18 @@ def execute(filters=None): validate_filters(filters) + from_date = filters.get('from_date') + to_date = filters.get('to_date') + include_uom = filters.get("include_uom") - columns = get_columns() + columns = get_columns(filters) items = get_items(filters) sle = get_stock_ledger_entries(filters, items) + if filters.get('show_stock_ageing_data'): + filters['show_warehouse_wise_stock'] = True + item_wise_fifo_queue = get_fifo_queue(filters, sle) + # if no stock ledger entry found return if not sle: return columns, [] @@ -29,7 +38,7 @@ def execute(filters=None): item_reorder_detail_map = get_item_reorder_details(item_map.keys()) data = [] - conversion_factors = [] + conversion_factors = {} for (company, item, warehouse) in sorted(iwb_map): if item_map.get(item): qty_dict = iwb_map[(company, item, warehouse)] @@ -39,36 +48,41 @@ def execute(filters=None): item_reorder_level = item_reorder_detail_map[item + warehouse]["warehouse_reorder_level"] item_reorder_qty = item_reorder_detail_map[item + warehouse]["warehouse_reorder_qty"] - report_data = [item, item_map[item]["item_name"], - item_map[item]["item_group"], - item_map[item]["brand"], - item_map[item]["description"], warehouse, - item_map[item]["stock_uom"], qty_dict.bal_qty, - qty_dict.bal_val, qty_dict.opening_qty, - qty_dict.opening_val, qty_dict.in_qty, - qty_dict.in_val, qty_dict.out_qty, - qty_dict.out_val, qty_dict.val_rate, - item_reorder_level, - item_reorder_qty, - company - ] - - if filters.get('show_variant_attributes', 0) == 1: - variants_attributes = get_variants_attributes() - report_data += [item_map[item].get(i) for i in variants_attributes] + report_data = { + 'item_code': item, + 'warehouse': warehouse, + 'company': company, + 'reorder_level': item_reorder_qty, + 'reorder_qty': item_reorder_qty, + } + report_data.update(item_map[item]) + report_data.update(qty_dict) if include_uom: - conversion_factors.append(item_map[item].conversion_factor) + conversion_factors.setdefault(item, item_map[item].conversion_factor) + + if filters.get('show_stock_ageing_data'): + fifo_queue = item_wise_fifo_queue[(item, warehouse)].get('fifo_queue') + + stock_ageing_data = { + 'average_age': 0, + 'earliest_age': 0, + 'latest_age': 0 + } + if fifo_queue: + fifo_queue = sorted(fifo_queue, key=lambda fifo_data: fifo_data[1]) + stock_ageing_data['average_age'] = get_average_age(fifo_queue, to_date) + stock_ageing_data['earliest_age'] = date_diff(to_date, fifo_queue[0][1]) + stock_ageing_data['latest_age'] = date_diff(to_date, fifo_queue[-1][1]) + + report_data.update(stock_ageing_data) data.append(report_data) - if filters.get('show_variant_attributes', 0) == 1: - columns += ["{}:Data:100".format(i) for i in get_variants_attributes()] - - update_included_uom_in_report(columns, data, include_uom, conversion_factors) + add_additional_uom_columns(columns, data, include_uom, conversion_factors) return columns, data -def get_columns(): +def get_columns(filters): """return columns""" columns = [ @@ -93,6 +107,14 @@ def get_columns(): {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 100} ] + if filters.get('show_stock_ageing_data'): + columns += [{'label': _('Average Age'), 'fieldname': 'average_age', 'width': 100}, + {'label': _('Earliest Age'), 'fieldname': 'earliest_age', 'width': 100}, + {'label': _('Latest Age'), 'fieldname': 'latest_age', 'width': 100}] + + if filters.get('show_variant_attributes'): + columns += [{'label': att_name, 'fieldname': att_name, 'width': 100} for att_name in get_variants_attributes()] + return columns def get_conditions(filters): @@ -130,11 +152,12 @@ def get_stock_ledger_entries(filters, items): return frappe.db.sql(""" select sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, - sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference + sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, + sle.item_code as name, sle.voucher_no from `tabStock Ledger Entry` sle force index (posting_sort_index) where sle.docstatus < 2 %s %s - order by sle.posting_date, sle.posting_time, sle.creation""" % + order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec (item_conditions_sql, conditions), as_dict=1) def get_item_warehouse_map(filters, sle): @@ -226,7 +249,7 @@ def get_item_details(items, sle, filters): cf_field = cf_join = "" if filters.get("include_uom"): cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom='%s'" \ + cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ % frappe.db.escape(filters.get("include_uom")) res = frappe.db.sql(""" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index aec37d4e94..4c663e393f 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -281,4 +281,36 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto def get_available_serial_nos(item_code, warehouse): return frappe.get_all("Serial No", filters = {'item_code': item_code, - 'warehouse': warehouse, 'delivery_document_no': ''}) or [] \ No newline at end of file + 'warehouse': warehouse, 'delivery_document_no': ''}) or [] + +def add_additional_uom_columns(columns, result, include_uom, conversion_factors): + if not include_uom or not conversion_factors: + return + + convertible_column_map = {} + for col_idx in list(reversed(range(0, len(columns)))): + col = columns[col_idx] + if isinstance(col, dict) and col.get('convertible') in ['rate', 'qty']: + next_col = col_idx + 1 + columns.insert(next_col, col.copy()) + columns[next_col]['fieldname'] += '_alt' + convertible_column_map[col.get('fieldname')] = frappe._dict({ + 'converted_col': columns[next_col]['fieldname'], + 'for_type': col.get('convertible') + }) + if col.get('convertible') == 'rate': + columns[next_col]['label'] += ' (per {})'.format(include_uom) + else: + columns[next_col]['label'] += ' ({})'.format(include_uom) + + for row_idx, row in enumerate(result): + for convertible_col, data in convertible_column_map.items(): + conversion_factor = conversion_factors[row.get('item_code')] or 1 + for_type = data.for_type + value_before_conversion = row.get(convertible_col) + if for_type == 'rate': + row[data.converted_col] = flt(value_before_conversion) * conversion_factor + else: + row[data.converted_col] = flt(value_before_conversion) / conversion_factor + + result[row_idx] = row \ No newline at end of file