feat: Negative Batch report

This commit is contained in:
Rohit Waghchaure
2026-02-17 15:07:37 +05:30
parent 83338675f9
commit 34edbed00b
4 changed files with 239 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Negative Batch Report"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_default("company"),
},
{
fieldname: "item_code",
label: __("Item Code"),
fieldtype: "Link",
options: "Item",
get_query: function () {
return {
filters: {
has_batch_no: 1,
},
};
},
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
get_query: function () {
return {
filters: {
is_group: 0,
company: frappe.query_report.get_filter_value("company"),
},
};
},
},
],
};

View File

@@ -0,0 +1,53 @@
{
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2026-02-17 11:34:21.549485",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "",
"letter_head": null,
"modified": "2026-02-17 11:34:59.106045",
"modified_by": "Administrator",
"module": "Stock",
"name": "Negative Batch Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial and Batch Bundle",
"report_name": "Negative Batch Report",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Purchase User"
},
{
"role": "Purchase Manager"
},
{
"role": "Stock User"
},
{
"role": "Stock Manager"
},
{
"role": "Delivery User"
},
{
"role": "Delivery Manager"
},
{
"role": "Manufacturing User"
},
{
"role": "Manufacturing Manager"
}
],
"timeout": 0
}

View File

@@ -0,0 +1,145 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import add_to_date, flt, today
from erpnext.stock.report.stock_ledger.stock_ledger import execute as stock_ledger_execute
def execute(filters: dict | None = None):
"""Return columns and data for the report.
This is the main entry point for the report. It accepts the filters as a
dictionary and should return columns and data. It is called by the framework
every time the report is refreshed or a filter is updated.
"""
columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns() -> list[dict]:
return [
{
"label": _("Posting Datetime"),
"fieldname": "posting_date",
"fieldtype": "Datetime",
"width": 160,
},
{
"label": _("Batch No"),
"fieldname": "batch_no",
"fieldtype": "Link",
"options": "Batch",
"width": 120,
},
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 150,
},
{
"label": _("Warehouse"),
"fieldname": "warehouse",
"fieldtype": "Link",
"options": "Warehouse",
"width": 160,
},
{
"label": _("Previous Qty"),
"fieldname": "previous_qty",
"fieldtype": "Float",
"width": 130,
},
{
"label": _("Transaction Qty"),
"fieldname": "actual_qty",
"fieldtype": "Float",
"width": 130,
},
{
"label": _("Qty After Transaction"),
"fieldname": "qty_after_transaction",
"fieldtype": "Float",
"width": 180,
},
{
"label": _("Document Type"),
"fieldname": "voucher_type",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Document No"),
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
"width": 130,
},
]
def get_data(filters) -> list[dict]:
batches = get_batches(filters)
companies = get_companies(filters)
batch_negative_data = []
flt_precision = frappe.db.get_default("float_precision") or 2
for company in companies:
for batch in batches:
_c, data = stock_ledger_execute(
frappe._dict(
{
"company": company,
"batch_no": batch,
"from_date": add_to_date(today(), years=-12),
"to_date": today(),
"segregate_serial_batch_bundle": 1,
"warehouse": filters.get("warehouse"),
"valuation_field_type": "Currency",
}
)
)
previous_qty = 0
for row in data:
if flt(row.get("qty_after_transaction"), flt_precision) < 0:
batch_negative_data.append(
{
"posting_date": row.get("date"),
"batch_no": row.get("batch_no"),
"item_code": row.get("item_code"),
"item_name": row.get("item_name"),
"warehouse": row.get("warehouse"),
"actual_qty": row.get("actual_qty"),
"qty_after_transaction": row.get("qty_after_transaction"),
"previous_qty": previous_qty,
"voucher_type": row.get("voucher_type"),
"voucher_no": row.get("voucher_no"),
}
)
previous_qty = row.get("qty_after_transaction")
return batch_negative_data
def get_batches(filters):
batch_filters = {}
if filters.get("item_code"):
batch_filters["item"] = filters["item_code"]
return frappe.get_all("Batch", pluck="name", filters=batch_filters)
def get_companies(filters):
company_filters = {}
if filters.get("company"):
company_filters["name"] = filters["company"]
return frappe.get_all("Company", pluck="name", filters=company_filters)