From 13fa5d8eb6288763a21b344576439b7f76964735 Mon Sep 17 00:00:00 2001 From: Raman Marikanti Date: Fri, 2 Jan 2026 09:59:48 +0530 Subject: [PATCH] Dashboard update --- .../dashboard/models/stock_dashboard.py | 13 +- .../pqgrid_stock_dashboard.js | 843 +++++++++++----- .../pqgrid_stock_dashboard.xml | 933 +++++++++++++++--- 3 files changed, 1442 insertions(+), 347 deletions(-) diff --git a/custom_addons/dashboard/models/stock_dashboard.py b/custom_addons/dashboard/models/stock_dashboard.py index 7e9380dec..e357bf6bd 100644 --- a/custom_addons/dashboard/models/stock_dashboard.py +++ b/custom_addons/dashboard/models/stock_dashboard.py @@ -141,9 +141,18 @@ class SamashtiDashboard(models.AbstractModel): return [] @api.model - def get_products_data(self): + def get_dashboard_cards_data(self): all_prod = self.env['product.product'].search([('type', '=', 'consu')]) - all_category = all_prod.category_id + all_category = all_prod.categ_id + out_stock_prods = all_prod.filtered(lambda x:x.qty_available <= 0) + low_stock_prods = self.env['stock.warehouse.orderpoint'].search([('qty_to_order', '>', 0)]).product_id + return { + 'products_count': [len(all_prod),all_prod.ids], + 'category_count':len(all_category), + 'out_stock_prods': [len(out_stock_prods),out_stock_prods.ids], + 'low_stock_prods': [len(low_stock_prods),low_stock_prods.ids], + } + @api.model diff --git a/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.js b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.js index 9b2027b81..3629ae813 100644 --- a/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.js +++ b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.js @@ -1,61 +1,157 @@ /** @odoo-module **/ -import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl"; +import { Component, onMounted, useState, onWillStart, useRef } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; import { registry } from "@web/core/registry"; import { standardActionServiceProps } from "@web/webclient/actions/action_service"; -function formatFloat(value) { - if (value === null || value === undefined || isNaN(value)) { - return "0.00"; - } - return parseFloat(value).toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }); -} - - export class StockDashboard extends Component { - static props = { + static props = { ...standardActionServiceProps, }; static template = "StockDashboard"; setup() { this.orm = useService("orm"); - this.gridRef = useRef("gridContainer"); + this.action = useService("action"); this.notification = useService("notification"); + // Refs for debouncing + this.filterTimeout = null; + this.state = useState({ rows: [], - category:[], + cards_data:[], + filteredRows: [], fromDate: "", - toDate: "" + toDate: "", + filters: { + productCode: '', + productName: '', + minStock: '', + maxStock: '' + }, + sortField: '', + sortDirection: 'asc', + activeCategory: 'Finished Products' }); onWillStart(async () => { - await this.loadDependencies(); this.initializeDates(); await this.loadGridData(); }); onMounted(() => { this.initDatePickers(); - if (this.gridRef.el) { - this.renderGrid(); - } }); } - async loadDependencies() { - try { - window.$ = window.jQuery = window.$ || window.jQuery; - if (!window.pq) throw new Error("pqGrid failed to load"); - } catch (error) { - console.error("Failed to load dependencies:", error); - this.notification.add("Failed to load required components", { type: "danger" }); - throw error; + // Get category groups (Finished Products vs Raw Materials & Packaging) + getCategoryGroups() { + const finishedProducts = this.getFilteredFinishedProducts(); + const rawMaterials = this.getFilteredRawMaterials(); + + return [ + { + name: 'Finished Products', + count: finishedProducts.length, + icon: 'fas fa-box', + color: 'primary', + badgeClass: 'bg-primary bg-opacity-10 text-primary', + slug: 'finished-products' + }, + { + name: 'Raw Materials & Packaging', + count: rawMaterials.length, + icon: 'fas fa-industry', + color: 'success', + badgeClass: 'bg-success bg-opacity-10 text-success', + slug: 'raw-materials-packaging' + } + ]; + } + + // Get ALL finished products (unfiltered) + getFinishedProducts() { + return this.state.rows.filter(row => + (row.category || '').toLowerCase() === 'finished products' + ); + } + + + + OpenAllProducts() { + let ids = this.state.cards_data['products_count'][1] || [] + this.action.doAction({ + type: "ir.actions.act_window", + name: ("Products"), + res_model: "product.product", + views: [[false, "list"]], + domain: [["id", "in", ids]], + target: 'current' + }); + } + OpenOutofProducts() { + let ids = this.state.cards_data['out_stock_prods'][1] || [] + this.action.doAction({ + type: "ir.actions.act_window", + name: ("Products"), + res_model: "product.product", + views: [[false, "list"]], + domain: [["id", "in", ids]], + target: 'current' + }); + } + + OpenLowProducts() { + let ids = this.state.cards_data['low_stock_prods'][1] || [] + this.action.doAction({ + type: "ir.actions.act_window", + name: ("Products"), + res_model: "stock.warehouse.orderpoint", + views: [[false, "list"]], + domain: [["product_id", "in", ids]], + target: 'current' + }); + } + + // Get FILTERED finished products + getFilteredFinishedProducts() { + let filtered = this.getFinishedProducts(); + filtered = this.applyCustomFilters(filtered); + return filtered; + } + + // Get ALL raw materials & packaging (unfiltered) + getRawMaterials() { + return this.state.rows.filter(row => { + const category = (row.category || '').toLowerCase(); + return category !== 'finished products' && category !== ''; + }); + } + + // Get FILTERED raw materials + getFilteredRawMaterials() { + let filtered = this.getRawMaterials(); + filtered = this.applyCustomFilters(filtered); + return filtered; + } + + // Switch active category + switchCategory(categoryName) { + this.state.activeCategory = categoryName; + // Trigger filter update to show correct filtered data + this.updateFilter(); + } + + // Format float values for display + formatFloat(value) { + if (value === null || value === undefined || isNaN(value)) { + return "0.00"; } + return parseFloat(value).toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); } initializeDates() { @@ -99,230 +195,507 @@ export class StockDashboard extends Component { this.state.toDate, ]); this.state.rows = records || []; - this.renderGrid(); + // Initialize filteredRows with filtered data + this.updateFilter(); + this.state.cards_data = await this.orm.call("samashti.board", "get_dashboard_cards_data",[]) + } catch (error) { console.error("Error loading data:", error); + this.notification.add("Error loading stock data", { type: "danger" }); } } - getTotalStockValue() { - return formatFloat(this.state.rows.reduce((sum, row) => sum + (row.value || 0), 0).toFixed(2)) + " ₹"; + + getTotalStockValue() { + return this.formatFloat(this.state.rows.reduce((sum, row) => sum + (row.value || 0), 0).toFixed(2)) + " ₹"; } -parseFloatValue(value) { - if (value === null || value === undefined || value === "") { + + parseFloatValue(value) { + if (value === null || value === undefined || value === "") { + return 0; + } + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string') { + let cleaned = value.replace(/[^\d.-]/g, ''); + let result = parseFloat(cleaned); + return isNaN(result) ? 0 : result; + } return 0; } - if (typeof value === 'number') { - return value; - } - if (typeof value === 'string') { - let cleaned = value.replace(/[^\d.-]/g, ''); - let result = parseFloat(cleaned); - return isNaN(result) ? 0 : result; - } - return 0; -} -// Stock dashboard calculation methods -getTotalProductsCount() { - // Count unique products based on product_code - const uniqueProducts = new Set( - this.state.rows - .filter(row => row.product_code) - .map(row => row.product_code) - ); - return uniqueProducts.size; -} - -getCategoryCount() { - // Count unique categories - const uniqueCategories = new Set( - this.state.rows - .filter(row => row.category) - .map(row => row.category) - ); - return uniqueCategories.size; -} - - - -getOutOfStockCount() { - return this.state.rows.filter(row => { - const currentStock = this.parseFloatValue(row.closing_stock || row.quantity || 0); - return currentStock <= 0; - }).length; -} - -getLowStockCount() { - return this.state.rows.filter(row => { - const currentStock = this.parseFloatValue(row.closing_stock || row.quantity || 0); - const minStock = this.parseFloatValue(row.min_stock) || 50; // Default minimum stock - return currentStock > 0 && currentStock < minStock; - }).length; -} - -getLowStockPercentage() { - const totalProducts = this.getTotalProductsCount(); - const lowStockCount = this.getLowStockCount(); - - if (totalProducts === 0) return "0.00"; - - const percentage = (lowStockCount / totalProducts) * 100; - return formatFloat(percentage); -} - async getColumns() { - return [ - { title: "Product Code", dataIndx: "product_code", width: 100, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, - { title: "Product Name", dataIndx: "product_name", width: 280, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, - { title: "Category", dataIndx: "category", width: 150 }, - { title: "Opening Stock", dataIndx: "opening_stock", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, - { title: "Receipts", dataIndx: "receipts", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, - { title: "Production", dataIndx: "production", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, - { title: "Consumption", dataIndx: "consumption", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, - { title: "Dispatch", dataIndx: "dispatch", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, - { title: "Closing Stock", dataIndx: "closing_stock", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, - { title: "Uom", dataIndx: "uom", width: 90, dataType: "text" }, - { title: "Value", dataIndx: "value", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, - ]; + getTotalProductsCount() { + const uniqueProducts = new Set( + this.state.rows + .filter(row => row.product_code) + .map(row => row.product_code) + ); + return uniqueProducts.size; } - async renderGrid() { - const columns = await this.getColumns(); - const agg = pq.aggregate; - - const gridOptions = { - selectionModel: { type: "row" }, - width: "100%", - height: "100%", - editable: false, - freezeCols: 2, - stripeRows: true, - menuIcon: true, //show header menu icon initially. - menuUI: { - tabs: ['filter'] //display only filter tab. - }, - filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1,menuIcon: true }, - dataModel: { data: this.state.rows, location: "local", sorting: "local", paging: "local" }, - colModel: columns, - toolbar: { - items: [ - { - type: 'select', - label: 'Format:', - attr: 'id="export_format" style="margin-left: 10px; margin-right: 20px; padding: 5px 10px; border: 1px solid #ddd; border-radius: 4px; background: #fafafa; color: #333; font-size: 13px; min-width: 110px;"', - options: [{ xlsx: '📊 Excel', csv: '📝 CSV', htm: '🌐 HTML', json: '🔤 JSON' }] - }, - { - type: "button", label: "Export", icon: "ui-icon-arrowthickstop-1-s", - attr: 'style="padding: 8px 16px; border: 1px solid #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"', - listener: () => this.exportGrid() - }, - { - type: 'button', - icon: 'ui-icon-print', - label: 'Print', - attr: 'style="padding: 8px 16px; border: 1px solid #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"', - listener: function () { - var exportHtml = this.exportData({ - title: 'Stock Report', - format: 'htm', - render: true - }), - newWin = window.open('', '', 'width=1200, height=700'), - doc = newWin.document.open(); - doc.write(exportHtml); - doc.close(); - newWin.print(); - } - }, - ], - }, - detailModel: { - cache: false, + getCategoryCount() { + const categories = new Set(); + this.state.rows.forEach(row => { + const category = (row.category || '').toLowerCase(); + if (category === 'finished products') { + categories.add('Finished Products'); + } else if (category) { + categories.add('Raw Materials & Packaging'); } - }; - - var grid = $(this.gridRef.el).pqGrid() - if (grid.length) { - grid.pqGrid("destroy"); - } - - $(this.gridRef.el) - .css({ height: '600px', width: '100%' }) - .pqGrid(gridOptions); - - const groupModel = { - on: true, - checkbox: true, - checkboxHead: true, - header: true, - cascade: true, - titleInFirstCol: true, - fixCols: true, - showSummary: [true, true], - grandSummary: true, - title: [ - "{0} ({1})", - "{0} - {1}" - ] - }; - $(this.gridRef.el).pqGrid("groupOption", groupModel); - $(this.gridRef.el).pqGrid("refreshDataAndView"); + }); + return categories.size; } - exportGrid() { - const format_ex = $("#export_format").val(); - const grid = $(this.gridRef.el); + getOutOfStockCount() { + return this.state.rows.filter(row => { + const currentStock = this.parseFloatValue(row.closing_stock || row.quantity || 0); + return currentStock <= 0; + }).length; + } - switch (format_ex) { - case 'xlsx': - const blobXlsx = grid.pqGrid("exportData", { - format: 'xlsx', - render: true - }); - saveAs(blobXlsx, "StockData.xlsx"); - break; + getLowStockCount() { + return this.state.rows.filter(row => { + const currentStock = this.parseFloatValue(row.closing_stock || row.quantity || 0); + const minStock = this.parseFloatValue(row.min_stock) || 50; + return currentStock > 0 && currentStock < minStock; + }).length; + } - case 'csv': - const blobCsv = grid.pqGrid("exportData", { - format: 'csv', - render: true - }); - if (typeof blobCsv === 'string') { - saveAs(new Blob([blobCsv], { type: 'text/csv' }), "StockData.csv"); - } else { - saveAs(blobCsv, "StockData.csv"); - } - break; + // Apply custom filters to a dataset + applyCustomFilters(data) { + let filtered = [...data]; - case 'htm': - case 'html': - const blobHtml = grid.pqGrid("exportData", { - format: 'htm', - render: true - }); - if (typeof blobHtml === 'string') { - saveAs(new Blob([blobHtml], { type: 'text/html' }), "StockData.html"); - } else { - saveAs(blobHtml, "StockData.html"); - } - break; - - case 'json': - const blobJson = grid.pqGrid("exportData", { - format: 'json', - render: true - }); - if (typeof blobJson === 'string') { - saveAs(new Blob([blobJson], { type: 'application/json' }), "StockData.json"); - } else { - saveAs(blobJson, "StockData.json"); - } - break; - - default: - alert('Unsupported format: ' + format_ex); + // Apply product code filter + if (this.state.filters.productCode.trim() !== '') { + filtered = filtered.filter(row => { + const productCode = row.product_code || ''; + return productCode.toLowerCase().includes(this.state.filters.productCode.toLowerCase().trim()); + }); } + + // Apply product name filter + if (this.state.filters.productName.trim() !== '') { + filtered = filtered.filter(row => { + const productName = row.product_name || ''; + return productName.toLowerCase().includes(this.state.filters.productName.toLowerCase().trim()); + }); + } + + // Apply stock range filters + if (this.state.filters.minStock !== '') { + const min = this.parseFloatValue(this.state.filters.minStock); + filtered = filtered.filter(row => { + const stock = this.parseFloatValue(row.closing_stock || row.quantity || 0); + return stock >= min; + }); + } + + if (this.state.filters.maxStock !== '') { + const max = this.parseFloatValue(this.state.filters.maxStock); + filtered = filtered.filter(row => { + const stock = this.parseFloatValue(row.closing_stock || row.quantity || 0); + return stock <= max; + }); + } + + // Apply sorting if specified + if (this.state.sortField) { + filtered.sort((a, b) => { + let valA = a[this.state.sortField]; + let valB = b[this.state.sortField]; + + // Handle undefined/null values + if (valA === null || valA === undefined) valA = ''; + if (valB === null || valB === undefined) valB = ''; + + if (typeof valA === 'string') { + valA = valA.toLowerCase(); + valB = valB.toLowerCase(); + } + + if (valA < valB) return this.state.sortDirection === 'asc' ? -1 : 1; + if (valA > valB) return this.state.sortDirection === 'asc' ? 1 : -1; + return 0; + }); + } + + return filtered; + } + + // Keyup filter handler with debouncing + onFilterKeyup(event) { + const field = event.target.placeholder.toLowerCase().includes('code') ? 'productCode' : + event.target.placeholder.toLowerCase().includes('name') ? 'productName' : + event.target.placeholder.toLowerCase().includes('min') ? 'minStock' : + event.target.placeholder.toLowerCase().includes('max') ? 'maxStock' : ''; + + if (field) { + this.state.filters[field] = event.target.value; + + // Clear existing timeout + if (this.filterTimeout) { + clearTimeout(this.filterTimeout); + } + + // Set new timeout for debouncing (300ms delay) + this.filterTimeout = setTimeout(() => { + this.updateFilter(); + }, 300); + } + } + + // Filter handler - called after debounce or button click + updateFilter() { + // Clear any pending timeout + if (this.filterTimeout) { + clearTimeout(this.filterTimeout); + this.filterTimeout = null; + } + + // Update filteredRows based on active category + if (this.state.activeCategory === 'Finished Products') { + const filtered = this.getFilteredFinishedProducts(); + this.state.filteredRows = filtered; + } else { + const filtered = this.getFilteredRawMaterials(); + this.state.filteredRows = filtered; + } + } + + clearFilters() { + // Clear timeout if any + if (this.filterTimeout) { + clearTimeout(this.filterTimeout); + this.filterTimeout = null; + } + + this.state.filters = { + productCode: '', + productName: '', + minStock: '', + maxStock: '' + }; + this.state.sortField = ''; + this.state.sortDirection = 'asc'; + + // Reset to show all data based on active category + if (this.state.activeCategory === 'Finished Products') { + this.state.filteredRows = this.getFinishedProducts(); + } else { + this.state.filteredRows = this.getRawMaterials(); + } + } + + sortTable(field) { + if (this.state.sortField === field) { + this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.state.sortField = field; + this.state.sortDirection = 'asc'; + } + + // Re-apply filters with new sort + this.updateFilter(); + } + + // Export methods + exportToCSV() { + let headers, rows; + + if (this.state.activeCategory === 'Finished Products') { + const filteredData = this.getFilteredFinishedProducts(); + headers = ['Product Code', 'Product Name', 'Opening Stock', 'Production', 'Dispatch', 'Closing Stock', 'UOM', 'Value']; + rows = filteredData.map(row => [ + `"${row.product_code || ''}"`, + `"${row.product_name || ''}"`, + row.opening_stock || 0, + row.production || 0, + row.dispatch || 0, + row.closing_stock || 0, + `"${row.uom || ''}"`, + row.value || 0 + ]); + } else { + const filteredData = this.getFilteredRawMaterials(); + headers = ['Product Code', 'Product Name', 'Opening Stock', 'Receipts', 'Consumption', 'Closing Stock', 'UOM', 'Value']; + rows = filteredData.map(row => [ + `"${row.product_code || ''}"`, + `"${row.product_name || ''}"`, + row.opening_stock || 0, + row.receipts || 0, + row.consumption || 0, + row.closing_stock || 0, + `"${row.uom || ''}"`, + row.value || 0 + ]); + } + + const csvContent = [ + headers.join(','), + ...rows.map(row => row.join(',')) + ].join('\n'); + + const fileName = `${this.state.activeCategory.toLowerCase().replace(/ /g, '-')}_${this.state.fromDate}_to_${this.state.toDate}.csv`; + this.downloadFile(csvContent, fileName, 'text/csv'); + } + + exportToExcel() { + let tableHeaders, rows; + + if (this.state.activeCategory === 'Finished Products') { + const filteredData = this.getFilteredFinishedProducts(); + tableHeaders = ` + + Product Code + Product Name + Opening Stock + Production + Dispatch + Closing Stock + UOM + Value + + `; + rows = filteredData.map(row => ` + + ${row.product_code || ''} + ${row.product_name || ''} + ${row.opening_stock || 0} + ${row.production || 0} + ${row.dispatch || 0} + ${row.closing_stock || 0} + ${row.uom || ''} + ${row.value || 0} + + `).join(''); + } else { + const filteredData = this.getFilteredRawMaterials(); + tableHeaders = ` + + Product Code + Product Name + Opening Stock + Receipts + Consumption + Closing Stock + UOM + Value + + `; + rows = filteredData.map(row => ` + + ${row.product_code || ''} + ${row.product_name || ''} + ${row.opening_stock || 0} + ${row.receipts || 0} + ${row.consumption || 0} + ${row.closing_stock || 0} + ${row.uom || ''} + ${row.value || 0} + + `).join(''); + } + + const tableHtml = ` + + + + + + +

${this.state.activeCategory} Report - ${this.state.fromDate} to ${this.state.toDate}

+ + ${tableHeaders} + ${rows} +
+ + + `; + + const fileName = `${this.state.activeCategory.toLowerCase().replace(/ /g, '-')}_${this.state.fromDate}_to_${this.state.toDate}.xls`; + this.downloadFile(tableHtml, fileName, 'application/vnd.ms-excel'); + } + + exportToJSON() { + let data; + + if (this.state.activeCategory === 'Finished Products') { + data = this.getFilteredFinishedProducts(); + } else { + data = this.getFilteredRawMaterials(); + } + + const jsonData = JSON.stringify(data, null, 2); + const fileName = `${this.state.activeCategory.toLowerCase().replace(/ /g, '-')}_${this.state.fromDate}_to_${this.state.toDate}.json`; + this.downloadFile(jsonData, fileName, 'application/json'); + } + + downloadFile(content, fileName, mimeType) { + const blob = new Blob([content], { type: mimeType }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + } + + printTable() { + const printWindow = window.open('', '_blank'); + + let tableHeaders, rows, totals; + + if (this.state.activeCategory === 'Finished Products') { + const finishedProducts = this.getFilteredFinishedProducts(); + tableHeaders = ` + + Product Code + Product Name + Opening Stock + Production + Dispatch + Closing Stock + UOM + Value + + `; + rows = finishedProducts.map(row => { + let rowClass = ''; + if (row.closing_stock <= 0) { + rowClass = 'danger'; + } else if (row.closing_stock < 50) { + rowClass = 'warning'; + } + return ` + + ${row.product_code || ''} + ${row.product_name || ''} + ${this.formatFloat(row.opening_stock)} + ${this.formatFloat(row.production)} + ${this.formatFloat(row.dispatch)} + ${this.formatFloat(row.closing_stock)} + ${row.uom || ''} + ${this.formatFloat(row.value)} ₹ + + `; + }).join(''); + + totals = ` + + TOTALS + ${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.opening_stock || 0), 0))} + ${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.production || 0), 0))} + ${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.dispatch || 0), 0))} + ${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.closing_stock || 0), 0))} + + ${this.formatFloat(finishedProducts.reduce((sum, row) => sum + (row.value || 0), 0))} ₹ + + `; + } else { + const rawMaterials = this.getFilteredRawMaterials(); + tableHeaders = ` + + Product Code + Product Name + Opening Stock + Receipts + Consumption + Closing Stock + UOM + Value + + `; + rows = rawMaterials.map(row => { + let rowClass = ''; + if (row.closing_stock <= 0) { + rowClass = 'danger'; + } else if (row.closing_stock < 50) { + rowClass = 'warning'; + } + return ` + + ${row.product_code || ''} + ${row.product_name || ''} + ${this.formatFloat(row.opening_stock)} + ${this.formatFloat(row.receipts)} + ${this.formatFloat(row.consumption)} + ${this.formatFloat(row.closing_stock)} + ${row.uom || ''} + ${this.formatFloat(row.value)} ₹ + + `; + }).join(''); + + totals = ` + + TOTALS + ${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.opening_stock || 0), 0))} + ${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.receipts || 0), 0))} + ${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.consumption || 0), 0))} + ${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.closing_stock || 0), 0))} + + ${this.formatFloat(rawMaterials.reduce((sum, row) => sum + (row.value || 0), 0))} ₹ + + `; + } + + printWindow.document.write(` + + + ${this.state.activeCategory} Report + + + +
+

${this.state.activeCategory} Report

+

Date Range: ${this.state.fromDate} to ${this.state.toDate}

+

Generated on: ${new Date().toLocaleString()}

+
+ + + ${tableHeaders} + + + ${rows} + + + ${totals} + +
+ + + `); + printWindow.document.close(); + printWindow.focus(); + setTimeout(() => { + printWindow.print(); + printWindow.close(); + }, 500); + } + + // Helper method to get sort icon + getSortIcon(field) { + if (this.state.sortField !== field) { + return 'fas fa-sort'; + } + return this.state.sortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'; } } -registry.category("actions").add("StockDashboard", StockDashboard); +registry.category("actions").add("StockDashboard", StockDashboard); \ No newline at end of file diff --git a/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.xml b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.xml index 917f70522..ccd0ca636 100644 --- a/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.xml +++ b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.xml @@ -1,135 +1,848 @@ -
-
-
-
- -
-
-
- -
-
-

Stock Dashboard

-

- - - - -

+
+
+ +
+
+
+ +
+
+
+ +
+
+

Stock Analytics Dashboard

+
+ + + + + + +
+
+
+
+ + +
+
+
+
+ + + + +
+
+ +
+
+ + + + +
+
+ +
+
+ +
+
+
+
+
-
- -
-
-
-
- - - - + +
+ +
+
+
+
+
+ +
+
+
TOTAL PRODUCTS
+

+
+ + +
+

+
+
-
-
- - - - + +
+
+
+
+
+ +
+
+
STOCK VALUE
+

+
+ + Total inventory value +
+

+
+
-
- + +
+
+
+
+
+ +
+
+
OUT OF STOCK
+

+
+ + Needs restocking +
+

+
+
+
+
+ + +
+
+
+
+
+ +
+
+
LOW STOCK
+

+
+ + Below minimum levels +
+

+
+
+
+ + +
+
+
Stock by Category
+ Click on a category to view detailed inventory +
+
+ + + + +
+ +
+ +
+
+
+
+
+ +
+
+
Finished Products
+ + Ready for dispatch products + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+ + Showing of records + + + Total Value: ₹ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
S.No + Product Code + + + Product Name + + Opening StockProductionDispatch + Closing Stock + + UOMValue
+ + + + + + + + + + + + + + + + + + ₹ + +
+
+ +

No finished products available

+ Try adjusting your filters or loading more data +
+
Totals + + + + + + + + + ₹ +
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+ +
+
+
Raw Materials & Packaging
+ + Production input materials + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+ + Showing of records + + + Total Value: ₹ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
S.No + Product Code + + + Product Name + + Opening StockReceiptsConsumption + Closing Stock + + UOMValue
+ + + + + + + + + + + + + + + + + + ₹ + +
+
+ +

No raw materials available

+ Try adjusting your filters or loading more data +
+
Totals + + + + + + + + + ₹ +
+
+
+ + +
+
+
+
+
+
-
-
-
+ \ No newline at end of file