diff --git a/erpnext/stock/report/reserved_stock/__init__.py b/erpnext/stock/report/reserved_stock/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.js b/erpnext/stock/report/reserved_stock/reserved_stock.js new file mode 100644 index 000000000000..e60c3b61dcf1 --- /dev/null +++ b/erpnext/stock/report/reserved_stock/reserved_stock.js @@ -0,0 +1,167 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Reserved Stock"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months( + frappe.datetime.get_today(), + -1 + ), + reqd: 1, + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + get_query: () => ({ + filters: { + is_stock_item: 1, + }, + }), + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + get_query: () => ({ + filters: { + is_group: 0, + company: frappe.query_report.get_filter_value("company"), + }, + }), + }, + { + fieldname: "stock_reservation_entry", + label: __("Stock Reservation Entry"), + fieldtype: "Link", + options: "Stock Reservation Entry", + get_query: () => ({ + filters: { + docstatus: 1, + company: frappe.query_report.get_filter_value("company"), + }, + }), + }, + { + fieldname: "voucher_type", + label: __("Voucher Type"), + fieldtype: "Link", + options: "DocType", + default: "Sales Order", + get_query: () => ({ + filters: { + name: ["in", ["Sales Order"]], + } + }), + }, + { + fieldname: "voucher_no", + label: __("Voucher No"), + fieldtype: "Dynamic Link", + options: "voucher_type", + get_query: () => ({ + filters: { + docstatus: 1, + company: frappe.query_report.get_filter_value("company"), + }, + }), + }, + { + fieldname: "against_pick_list", + label: __("Against Pick List"), + fieldtype: "Link", + options: "Pick List", + get_query: () => ({ + filters: { + docstatus: 1, + company: frappe.query_report.get_filter_value("company"), + }, + }), + }, + { + fieldname: "reservation_based_on", + label: __("Reservation Based On"), + fieldtype: "Select", + options: ["", "Qty", "Serial and Batch"], + }, + { + fieldname: "status", + label: __("Status"), + fieldtype: "Select", + options: [ + "", + "Partially Reserved", + "Reserved", + "Partially Delivered", + "Delivered", + ], + }, + { + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project", + get_query: () => ({ + filters: { + company: frappe.query_report.get_filter_value("company"), + }, + }), + }, + ], + formatter: (value, row, column, data, default_formatter) => { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "status") { + switch (data.status) { + case "Partially Reserved": + value = "" + value + ""; + break; + case "Reserved": + value = "" + value + ""; + break; + case "Partially Delivered": + value = "" + value + ""; + break; + case "Delivered": + value = "" + value + ""; + break; + } + } + else if (column.fieldname == "delivered_qty") { + if (data.delivered_qty > 0) { + if (data.reserved_qty > data.delivered_qty) { + value = "" + value + ""; + } + else { + value = "" + value + ""; + } + } + else { + value = "" + value + ""; + } + } + + return value; + }, +}; diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.json b/erpnext/stock/report/reserved_stock/reserved_stock.json new file mode 100644 index 000000000000..17b916afda80 --- /dev/null +++ b/erpnext/stock/report/reserved_stock/reserved_stock.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-08-02 22:11:19.439620", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2023-08-03 12:46:33.780222", + "modified_by": "Administrator", + "module": "Stock", + "name": "Reserved Stock", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Reservation Entry", + "report_name": "Reserved Stock", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.py b/erpnext/stock/report/reserved_stock/reserved_stock.py new file mode 100644 index 000000000000..72f7623b556b --- /dev/null +++ b/erpnext/stock/report/reserved_stock/reserved_stock.py @@ -0,0 +1,192 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.query_builder.functions import Date + + +def execute(filters=None): + columns, data = [], [] + + validate_filters(filters) + + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def validate_filters(filters): + if not filters: + frappe.throw(_("Please set filters")) + + for field in ["company", "from_date", "to_date"]: + if not filters.get(field): + frappe.throw(_("Please set {0}").format(field)) + + if filters.get("from_date") > filters.get("to_date"): + frappe.throw(_("From Date cannot be greater than To Date")) + + +def get_data(filters): + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select( + sre.creation, + sre.warehouse, + sre.item_code, + sre.stock_uom, + sre.voucher_qty, + sre.reserved_qty, + sre.delivered_qty, + (sre.available_qty - sre.reserved_qty).as_("available_qty"), + sre.voucher_type, + sre.voucher_no, + sre.against_pick_list, + sre.name.as_("stock_reservation_entry"), + sre.status, + sre.project, + sre.company, + ) + .where( + (sre.docstatus == 1) + & (sre.company == filters.get("company")) + & ( + (Date(sre.creation) >= filters.get("from_date")) + & (Date(sre.creation) <= filters.get("to_date")) + ) + ) + ) + + for field in [ + "company", + "item_code", + "warehouse", + "voucher_type", + "voucher_no", + "against_pick_list", + "reservation_based_on", + "status", + "project", + ]: + if value := filters.get(field): + query = query.where((sre[field] == value)) + + if value := filters.get("stock_reservation_entry"): + query = query.where((sre.name == value)) + + data = query.run(as_list=True) + + return data + + +def get_columns(): + columns = [ + { + "label": _("Date"), + "fieldname": "date", + "fieldtype": "Datetime", + "width": 150, + }, + { + "fieldname": "warehouse", + "label": _("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "width": 150, + }, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + { + "fieldname": "stock_uom", + "label": _("Stock UOM"), + "fieldtype": "Link", + "options": "UOM", + "width": 100, + }, + { + "fieldname": "voucher_qty", + "label": _("Voucher Qty"), + "fieldtype": "Float", + "width": 110, + "convertible": "qty", + }, + { + "fieldname": "reserved_qty", + "label": _("Reserved Qty"), + "fieldtype": "Float", + "width": 110, + "convertible": "qty", + }, + { + "fieldname": "delivered_qty", + "label": _("Delivered Qty"), + "fieldtype": "Float", + "width": 110, + "convertible": "qty", + }, + { + "fieldname": "available_qty", + "label": _("Available Qty to Reserve"), + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "fieldname": "voucher_type", + "label": _("Voucher Type"), + "fieldtype": "Data", + "options": "Warehouse", + "width": 110, + }, + { + "fieldname": "voucher_no", + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 120, + }, + { + "fieldname": "against_pick_list", + "label": _("Against Pick List"), + "fieldtype": "Link", + "options": "Pick List", + "width": 130, + }, + { + "fieldname": "stock_reservation_entry", + "label": _("Stock Reservation Entry"), + "fieldtype": "Link", + "options": "Stock Reservation Entry", + "width": 150, + }, + { + "fieldname": "status", + "label": _("Status"), + "fieldtype": "Data", + "width": 120, + }, + { + "fieldname": "project", + "label": _("Project"), + "fieldtype": "Link", + "options": "Project", + "width": 100, + }, + { + "fieldname": "company", + "label": _("Company"), + "fieldtype": "Link", + "options": "Company", + "width": 110, + }, + ] + + return columns diff --git a/erpnext/stock/report/reserved_stock/test_reserved_stock.py b/erpnext/stock/report/reserved_stock/test_reserved_stock.py new file mode 100644 index 000000000000..f408c0f860ef --- /dev/null +++ b/erpnext/stock/report/reserved_stock/test_reserved_stock.py @@ -0,0 +1,51 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from random import randint + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings + +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.stock_reservation_entry.test_stock_reservation_entry import ( + cancel_all_stock_reservation_entries, + create_items, + create_material_receipt, +) +from erpnext.stock.report.reserved_stock.reserved_stock import get_data as reserved_stock_report + + +class TestReservedStock(FrappeTestCase): + def setUp(self) -> None: + super().setUp() + self.stock_qty = 100 + self.warehouse = "_Test Warehouse - _TC" + + def tearDown(self) -> None: + cancel_all_stock_reservation_entries() + return super().tearDown() + + @change_settings( + "Stock Settings", + { + "allow_negative_stock": 0, + "enable_stock_reservation": 1, + "auto_reserve_serial_and_batch": 1, + "pick_serial_and_batch_based_on": "FIFO", + }, + ) + def test_reserved_stock_report(self): + items_details = create_items() + create_material_receipt(items_details, self.warehouse, qty=self.stock_qty) + + for item_code, properties in items_details.items(): + so = make_sales_order( + item_code=item_code, qty=randint(11, 100), warehouse=self.warehouse, uom=properties.stock_uom + ) + so.create_stock_reservation_entries() + + data = reserved_stock_report(filters={"warehouse": self.warehouse}) + self.assertEqual(len(data), len(items_details)) + + for d in data: + self.assertEqual(d.voucher_qty, d.reserved_qty) + self.assertEqual(d.available_qty, self.stock_qty - d.reserved_qty)