From 5581b94b64d0246eac630e96a554355e438364ca Mon Sep 17 00:00:00 2001 From: Raman Marikanti Date: Thu, 12 Feb 2026 14:53:38 +0530 Subject: [PATCH] DPR --- .../reports/customer_orders_report.py | 145 +- .../static/src/js/dashboard.js | 1567 +++++++++++------ .../static/src/xml/dashboard.xml | 257 ++- 3 files changed, 1239 insertions(+), 730 deletions(-) diff --git a/custom_addons/customer_orders/reports/customer_orders_report.py b/custom_addons/customer_orders/reports/customer_orders_report.py index e0fb1eae1..f7e652a38 100644 --- a/custom_addons/customer_orders/reports/customer_orders_report.py +++ b/custom_addons/customer_orders/reports/customer_orders_report.py @@ -43,6 +43,9 @@ class CustomerOrdersReport(models.AbstractModel): from odoo import models, fields, api from datetime import datetime, timedelta import json +from datetime import date, datetime +import calendar + class CORDashboard(models.AbstractModel): @@ -257,107 +260,65 @@ class CORDashboard(models.AbstractModel): except Exception as e: return [] - -# Add this method to your cor.dashboard model - - def get_dpr_daily_production_data(self): - """Get DPR (Daily Production Report) data""" + @api.model + def get_dpr_daily_production_data(self, month, year): try: - # Get current date - today = fields.Date.today() - first_day_of_month = today.replace(day=1) - last_day_of_prev_month = first_day_of_month - timedelta(days=1) - first_day_of_prev_month = last_day_of_prev_month.replace(day=1) + month = str(int(month)) + year = str(int(year)) + results = [] - # Initialize result - result = [] - - # Get all products - products = self.env['product.product'].search([ - ('type', '=', 'product'), - ('categ_id.complete_name', 'ilike', 'Finished Goods') + # Search for customer orders in selected month/year + co = self.env['customer.order'].search([ + ('order_month', '=', month), + ('order_year', '=', year) ]) - for product in products: - # Get pending orders from last month - pending_orders = self.env['sale.order.line'].search([ - ('product_id', '=', product.id), - ('order_id.state', 'in', ['sale', 'done']), - ('order_id.date_order', '>=', first_day_of_prev_month), - ('order_id.date_order', '<=', last_day_of_prev_month), - ('qty_delivered', '<', 'product_uom_qty') + for line in co.order_line_ids: + month_int = int(line.order_id.order_month) + year_int = int(line.order_id.order_year) + + first_day = date(year_int, month_int, 1) + last_day = date(year_int, month_int, + calendar.monthrange(year_int, month_int)[1]) + + # Convert to datetime for date_start (Datetime field) + start_datetime = datetime.combine(first_day, datetime.min.time()) + end_datetime = datetime.combine(last_day, datetime.max.time()) + + # Get all production orders for this customer order's products + production_orders = self.env['mrp.production'].search_read([ + ('product_id', '=', line.product_id.id), + ('date_start', '>=', start_datetime), + ('date_start', '<=', end_datetime), + ], ['date_start', 'product_uom_qty']) + + # Calculate pending orders from previous months + pending_orders = self.env['customer.order.line'].search([ + ('product_id', '=', line.product_id.id), + ('order_id.customer_id', '=', line.order_id.customer_id.id), + ('order_month', '!=', month), + ('pending_dispatch', '>', 0) ]) - pending_qty = sum(pending_orders.mapped('product_uom_qty')) - sum(pending_orders.mapped('qty_delivered')) - pending_value = pending_qty * product.standard_price + pending_order_qty = sum(pending_orders.mapped('pending_dispatch')) - # Get present month orders - present_orders = self.env['sale.order.line'].search([ - ('product_id', '=', product.id), - ('order_id.state', 'in', ['sale', 'done']), - ('order_id.date_order', '>=', first_day_of_month), - ('order_id.date_order', '<=', today) - ]) - - present_qty = sum(present_orders.mapped('product_uom_qty')) - present_value = present_qty * product.standard_price - - # Get FG available quantity - fg_qty = product.qty_available - fg_value = fg_qty * product.standard_price - - # Get dispatch qty for current month - moves = self.env['stock.move'].search([ - ('product_id', '=', product.id), - ('state', '=', 'done'), - ('date', '>=', first_day_of_month), - ('date', '<=', today), - ('location_dest_id.usage', '=', 'customer'), - ('location_id.usage', '=', 'internal') - ]) - - dispatch_qty = sum(moves.mapped('quantity_done')) - dispatch_value = dispatch_qty * product.standard_price - - # Get daily production for last 31 days - daily_data = {} - for i in range(31, 0, -1): - day_date = today - timedelta(days=(31 - i)) - day_moves = self.env['stock.move'].search([ - ('product_id', '=', product.id), - ('state', '=', 'done'), - ('date', '>=', day_date), - ('date', '<', day_date + timedelta(days=1)), - ('production_id', '!=', False) # Production moves - ]) - daily_data[f'day{i}_qty'] = sum(day_moves.mapped('quantity_done')) - - # Calculate totals - total_order_qty = pending_qty + present_qty - total_order_value = pending_value + present_value - remaining_qty = max(0, total_order_qty - fg_qty) - remaining_value = max(0, total_order_value - fg_value) - - result.append({ - 'product_id': product.id, - 'product_name': product.display_name, - 'pending_order_qty': pending_qty, - 'pending_order_value': pending_value, - 'present_month_order_qty': present_qty, - 'present_month_order_value': present_value, - 'total_order_qty': total_order_qty, - 'total_order_value': total_order_value, - 'fg_available_qty': fg_qty, - 'fg_available_value': fg_value, - 'dispatch_qty': dispatch_qty, - 'dispatch_value': dispatch_value, - 'remaining_to_produce_qty': remaining_qty, - 'remaining_to_produce_value': remaining_value, - **daily_data + results.append({ + 'customer': line.order_id.customer_id.name, + 'product': line.product_id.display_name, + 'order_qty': line.order_qty, + 'order_qty_kg': line.order_qty * line.product_id.weight, + 'fg_qty': line.fg_available, + 'pending_order': pending_order_qty, + 'produced_qty': line.produced_qty, + 'produced_qty_kg': line.produced_qty * line.product_id.weight, + 'dispatch_qty': line.dispatched_qty, + 'dispatched_qty_kg': line.dispatched_qty * line.product_id.weight, + 'remaining_production': line.order_qty - (line.produced_qty + line.fg_available), + 'date_wise': production_orders }) - return result + return results except Exception as e: - # _logger.error(f"Error in get_dpr_daily_production_data: {str(e)}") + # logger.error(f"Error in get_dpr_daily_production_data: {str(e)}") return [] \ No newline at end of file diff --git a/custom_addons/customer_orders/static/src/js/dashboard.js b/custom_addons/customer_orders/static/src/js/dashboard.js index 93261bb7d..a37aafed3 100644 --- a/custom_addons/customer_orders/static/src/js/dashboard.js +++ b/custom_addons/customer_orders/static/src/js/dashboard.js @@ -1,13 +1,11 @@ /** @odoo-module **/ + import { useService } from "@web/core/utils/hooks"; import { standardActionServiceProps } from "@web/webclient/actions/action_service"; import { registry } from "@web/core/registry"; import { Component, onMounted, useRef, onWillStart, useState } from "@odoo/owl"; import { loadCSS } from '@web/core/assets'; - - - export class CustomerOrderStatusGrid extends Component { static template = "CustomerOrderStatusGrid"; static props = { @@ -40,7 +38,7 @@ export class CustomerOrderStatusGrid extends Component { completionRate: 0 }, - // Dispatch Summary Data + // Dispatch Summary Data - Enhanced with day-wise columns dispatchData: [], dispatchLoading: false, dispatchSummary: { @@ -49,6 +47,7 @@ export class CustomerOrderStatusGrid extends Component { partialDispatch: 0, totalBalanceQty: 0 }, + dispatchDateColumns: [], // Store date columns for dispatch grid // Raw Material Data rmData: [], @@ -70,7 +69,8 @@ export class CustomerOrderStatusGrid extends Component { totalFgAvailable: 0, totalRemainingProduction: 0 }, - selectedMonthYear:currentMonthYear + dprDateColumns: [], + selectedMonthYear: currentMonthYear }); this.orderGridRef = useRef('orderGrid'); @@ -79,28 +79,48 @@ export class CustomerOrderStatusGrid extends Component { this.dprGridRef = useRef('dprGrid'); onWillStart(async () => { - await this.loadGridData(); -// await loadCSS('/web_grid/static/lib/pq_grid/themes/gray/pqgrid.css') - + await this.loadAllData(); + await this.refreshOrderStatus() + // await loadCSS('/web_grid/static/lib/pq_grid/themes/gray/pqgrid.css') }); onMounted(() => { this.initializeTabs(); - this.initializeOrderGrid() }); } + // ==================== + // FILTER METHODS + // ==================== + onMonthYearChange(event) { this.state.selectedMonthYear = event.target.value; } - clearFilter() { - this.state.selectedMonthYear = ''; - this.loadGridData(); - this.notification.add("Filter cleared", { type: "success" }); + async applyFilter() { + if (!this.state.selectedMonthYear) { + this.notification.add("Please select a month/year", { + type: "warning", + title: "Filter" + }); + return; + } + await this.loadAllData(); + this.notification.add(`Filter applied for ${this.getMonthYearLabel()}`, { + type: "success", + title: "Filter" + }); + } + + clearFilter() { + const currentDate = new Date(); + const currentMonth = String(currentDate.getMonth() + 1).padStart(2, '0'); + const currentYear = currentDate.getFullYear(); + this.state.selectedMonthYear = `${currentYear}-${currentMonth}`; + this.loadAllData(); + this.notification.add("Filter reset to current month", { type: "success" }); } -// Helper method to format month/year label getMonthYearLabel() { if (!this.state.selectedMonthYear) return ''; const [year, month] = this.state.selectedMonthYear.split('-'); @@ -110,7 +130,11 @@ export class CustomerOrderStatusGrid extends Component { ]; return `${monthNames[parseInt(month) - 1]} ${year}`; } - // Helper functions - make them available to template + + // ==================== + // FORMATTING HELPERS + // ==================== + formatFloat(value) { if (value === null || value === undefined || isNaN(value)) { return "0.00"; @@ -129,6 +153,25 @@ export class CustomerOrderStatusGrid extends Component { return parseInt(value).toLocaleString('en-US'); } + formatDate(date) { + if (!date) return "N/A"; + return date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + ' ' + date.toLocaleDateString(); + } + + getCompletionRateClass(rate) { + if (rate >= 100) return 'badge-success'; + if (rate >= 80) return 'badge-warning'; + return 'badge-danger'; + } + + // ==================== + // TAB INITIALIZATION + // ==================== + initializeTabs() { if (window.bootstrap) { const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]'); @@ -150,18 +193,16 @@ export class CustomerOrderStatusGrid extends Component { let month = ''; let year = ''; - // If filter is set, use it - if (this.state.selectedMonthYear) { - [year, month] = this.state.selectedMonthYear.split('-'); - } - - const data = await this.orm.call( - "cor.dashboard", - "get_customer_order_status_data", - [month,year], - {} - ); + if (this.state.selectedMonthYear) { + [year, month] = this.state.selectedMonthYear.split('-'); + } + const data = await this.orm.call( + "cor.dashboard", + "get_customer_order_status_data", + [month, year], + {} + ); let totalOrderQty = 0; let totalProducedQty = 0; @@ -182,12 +223,13 @@ export class CustomerOrderStatusGrid extends Component { order_qty_kg: parseFloat(row.order_qty_kg) || 0, produced_qty: parseFloat(row.produced_qty), produced_qty_kg: parseFloat(row.produced_qty_kg) || 0, - dispatched_qty: parseFloat(row.dispatched_qty), + dispatched_qty: parseFloat(row.dispatched_qty), dispatched_qty_kg: parseFloat(row.dispatched_qty_kg) || 0, fg_qty: parseFloat(row.fg_qty) || 0, remaining_to_produce: Math.max(0, parseFloat(row.order_qty) - (parseFloat(row.produced_qty) + parseFloat(row.fg_qty))), remaining_dispatch_qty: Math.max(0, parseFloat(row.order_qty) - parseFloat(row.dispatched_qty)), - completion_rate: parseFloat(row.dispatched_qty) > 0 ? ((parseFloat(row.dispatched_qty) / parseFloat(row.order_qty)) * 100) : 0 + completion_rate: parseFloat(row.dispatched_qty) > 0 ? + ((parseFloat(row.dispatched_qty) / parseFloat(row.order_qty)) * 100) : 0 }; }); @@ -203,6 +245,11 @@ export class CustomerOrderStatusGrid extends Component { completionRate }; + // Initialize grid after data load + if (this.state.activeTab === 'order') { + setTimeout(() => this.initializeOrderGrid(), 100); + } + } catch (error) { console.error("Error loading grid data:", error); this.notification.add("Failed to load customer order status data", { @@ -215,27 +262,61 @@ export class CustomerOrderStatusGrid extends Component { } // ==================== - // DISPATCH SUMMARY METHODS + // DISPATCH SUMMARY METHODS - ENHANCED WITH DAY-WISE COLUMNS // ==================== async loadDispatchSummary() { this.state.dispatchLoading = true; try { - - let month = ''; + let month = ''; let year = ''; - // If filter is set, use it - if (this.state.selectedMonthYear) { - [year, month] = this.state.selectedMonthYear.split('-'); - } + if (this.state.selectedMonthYear) { + [year, month] = this.state.selectedMonthYear.split('-'); + } + const data = await this.orm.call( "cor.dashboard", "get_dispatch_summary_data", - [month,year], + [month, year], {} ); + // Generate day columns for the selected month + const dayColumns = []; + if (this.state.selectedMonthYear) { + const [year, month] = this.state.selectedMonthYear.split('-'); + const daysInMonth = new Date(parseInt(year), parseInt(month), 0).getDate(); + + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${year}-${month.padStart(2, '0')}-${day.toString().padStart(2, '0')}`; + const dayStr = `${day.toString().padStart(2, '0')}/${month}/${year}`; + + dayColumns.push({ + title: dayStr, + width: 90, + dataIndx: dateStr, + dataType: "float", + align: "right", + summary: { type: "sum_" }, + render: (ui) => { + const value = ui.cellData; + if (value === null || + value === undefined || + value === 0 || + value === '' || + isNaN(parseFloat(value)) || + !isFinite(value)) { + return '-'; + } + return this.formatInteger(value); + }, + style: { backgroundColor: '#f8f9fa', textAlign: 'right' } + }); + } + } + this.state.dispatchDateColumns = dayColumns; + let totalOrders = 0; let fullDispatch = 0; let partialDispatch = 0; @@ -247,7 +328,6 @@ export class CustomerOrderStatusGrid extends Component { const balanceQty = Math.max(0, orderQty - dispatchedQty); const dispatchStatus = balanceQty === 0 ? "Full" : "Partial"; - totalOrders++; if (dispatchStatus === "Full") { fullDispatch++; @@ -256,14 +336,41 @@ export class CustomerOrderStatusGrid extends Component { } totalBalanceQty += balanceQty; + // Initialize day-wise dispatch quantities + const dailyDispatch = {}; + if (this.state.selectedMonthYear) { + const [year, month] = this.state.selectedMonthYear.split('-'); + const daysInMonth = new Date(parseInt(year), parseInt(month), 0).getDate(); + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${year}-${month.padStart(2, '0')}-${day.toString().padStart(2, '0')}`; + dailyDispatch[dateStr] = 0; + } + } + + // Fill actual dispatch quantities from invoice_date_qty array + if (row.invoice_date_qty && Array.isArray(row.invoice_date_qty)) { + row.invoice_date_qty.forEach(inv => { + if (inv.invoice_date) { + // Format date to match our column keys + const invDate = inv.invoice_date.split(' ')[0]; // Remove time if present + if (dailyDispatch.hasOwnProperty(invDate)) { + dailyDispatch[invDate] += parseFloat(inv.qty) || 0; + } + } + }); + } + return { ...row, customer: row.customer || row.customer_name || "", product: row.product || row.product_name || "", order_qty: orderQty, - dispatched_qty: dispatchedQty, + order_qty_kg: parseFloat(row.order_qty_kg) || 0, + invoiced_qty: dispatchedQty, + invoiced_qty_kg: parseFloat(row.invoiced_qty_kg) || 0, balance_qty: balanceQty, - dispatch_status: dispatchStatus + dispatch_status: dispatchStatus, + ...dailyDispatch // Spread daily dispatch quantities }; }); @@ -275,6 +382,11 @@ export class CustomerOrderStatusGrid extends Component { totalBalanceQty }; + // Initialize grid after data load + if (this.state.activeTab === 'dispatch') { + setTimeout(() => this.initializeDispatchGrid(), 100); + } + } catch (error) { console.error("Error loading dispatch summary data:", error); this.notification.add("Failed to load dispatch summary data", { @@ -294,14 +406,13 @@ export class CustomerOrderStatusGrid extends Component { async loadRMAvailability() { this.state.rmLoading = true; try { - - let month = ''; + let month = ''; let year = ''; - // If filter is set, use it - if (this.state.selectedMonthYear) { - [year, month] = this.state.selectedMonthYear.split('-'); - } + if (this.state.selectedMonthYear) { + [year, month] = this.state.selectedMonthYear.split('-'); + } + const data = await this.orm.call( "cor.dashboard", "get_raw_material_availability_data", @@ -343,6 +454,11 @@ export class CustomerOrderStatusGrid extends Component { totalShortageQty }; + // Initialize grid after data load + if (this.state.activeTab === 'rm') { + setTimeout(() => this.initializeRMGrid(), 100); + } + } catch (error) { console.error("Error loading RM availability data:", error); this.notification.add("Failed to load raw material availability data", { @@ -362,11 +478,17 @@ export class CustomerOrderStatusGrid extends Component { async loadDPRData() { this.state.dprLoading = true; try { - // Call backend method to get DPR data + let month = ''; + let year = ''; + + if (this.state.selectedMonthYear) { + [year, month] = this.state.selectedMonthYear.split('-'); + } + const data = await this.orm.call( "cor.dashboard", - "get_dpr_data", - [[]], + "get_dpr_daily_production_data", + [month, year], {} ); @@ -377,36 +499,71 @@ export class CustomerOrderStatusGrid extends Component { let totalFgAvailable = 0; let totalRemainingProduction = 0; - // Generate day columns for last 31 days - const today = new Date(); - const dayColumns = {}; + // Generate day columns based on selected month + const dayColumns = []; + if (this.state.selectedMonthYear) { + const [year, month] = this.state.selectedMonthYear.split('-'); + const daysInMonth = new Date(parseInt(year), parseInt(month), 0).getDate(); - for (let i = 1; i <= 31; i++) { - const date = new Date(today); - date.setDate(date.getDate() - (31 - i)); - const dayKey = `day${i}`; - const dayLabel = date.toLocaleDateString('en-US', { - weekday: 'short', - day: 'numeric' - }); - dayColumns[dayKey] = { - label: dayLabel, - date: date - }; + for (let day = 1; day <= daysInMonth; day++) { + const dayStr = `${day.toString().padStart(2, '0')}/${month}/${year}`; + const dayKey = `day_${day}`; + + dayColumns.push({ + title: dayStr, + width: 90, + dataIndx: dayKey, + dataType: "float", + align: "right", + summary: { type: "sum_" }, + render: (ui) => { + const value = ui.cellData; + if (value === null || + value === undefined || + value === 0 || + value === '' || + isNaN(parseFloat(value)) || + !isFinite(value)) { + return '-'; + } + return this.formatInteger(value); + }, + style: { backgroundColor: '#f8f9fa', textAlign: 'right' } + }); + } } + this.state.dprDateColumns = dayColumns; const processedData = data.map(row => { const pendingOrder = parseFloat(row.pending_order) || 0; - const currentOrder = parseFloat(row.current_month_order) || 0; + const currentOrder = parseFloat(row.order_qty) || 0; const orderQty = pendingOrder + currentOrder; - const fgAvailable = parseFloat(row.fg_available) || 0; + const fgAvailable = parseFloat(row.fg_qty) || 0; const remainingProduction = Math.max(0, orderQty - fgAvailable); - // Get daily production data + // Process date-wise production data const dailyProduction = {}; - for (let i = 1; i <= 31; i++) { - const dayKey = `day${i}`; - dailyProduction[dayKey] = parseFloat(row[dayKey]) || 0; + + // Initialize all days with 0 + if (this.state.selectedMonthYear) { + const [year, month] = this.state.selectedMonthYear.split('-'); + const daysInMonth = new Date(parseInt(year), parseInt(month), 0).getDate(); + for (let day = 1; day <= daysInMonth; day++) { + dailyProduction[`day_${day}`] = 0; + } + } + + // Fill actual production quantities + if (row.date_wise && Array.isArray(row.date_wise)) { + row.date_wise.forEach(prod => { + if (prod.date_start) { + const prodDate = new Date(prod.date_start); + const day = prodDate.getDate(); + const dayKey = `day_${day}`; + dailyProduction[dayKey] = (dailyProduction[dayKey] || 0) + + (parseFloat(prod.product_uom_qty) || 0); + } + }); } totalProducts++; @@ -417,13 +574,17 @@ export class CustomerOrderStatusGrid extends Component { totalRemainingProduction += remainingProduction; return { - product: row.product || row.product_name || "", + product: row.product || "", + customer: row.customer, pending_order: pendingOrder, current_month_order: currentOrder, + current_month_order_kg: parseFloat(row.order_qty_kg) || 0, total_order_qty: orderQty, - fg_available: fgAvailable, + fg_qty: fgAvailable, dispatch_qty: parseFloat(row.dispatch_qty) || 0, + dispatched_qty_kg: parseFloat(row.dispatched_qty_kg) || 0, remaining_production: remainingProduction, + date_wise: row.date_wise, ...dailyProduction }; }); @@ -435,10 +596,14 @@ export class CustomerOrderStatusGrid extends Component { totalCurrentOrder, totalOrderQty, totalFgAvailable, - totalRemainingProduction, - dayColumns: dayColumns + totalRemainingProduction }; + // Initialize grid after data load + if (this.state.activeTab === 'dpr') { + setTimeout(() => this.initializeDPRGrid(), 100); + } + } catch (error) { console.error("Error loading DPR data:", error); this.notification.add("Failed to load DPR data", { @@ -456,269 +621,326 @@ export class CustomerOrderStatusGrid extends Component { // ==================== initializeOrderGrid() { - if (this.orderGridRef.el && this.state.gridData.length > 0) { - const columns = this.getOrderGridColumns(); - - // Check if paramquery is loaded - if (typeof window.$ === 'undefined' || typeof window.pq === 'undefined') { - console.warn('jQuery or paramQuery not loaded'); - return; - } - - - $(this.orderGridRef.el).pqGrid({ - width: "100%", - height: 500, - title: "Customer Order Status", - editable:false, - dataModel: { - data: this.state.gridData, - location: "local", - sorting: "local", - paging: "local", - rPP: 50 - }, - - summaryModel: { - on: true, - type: 'top', - title: 'Grand Total:', - col: ['order_qty', 'produced_qty', 'dispatched_qty'] - }, - colModel: columns, - selectionModel: { - type: "row", - mode: "single" - }, - filterModel: { - on: true, - mode: "AND", - header: true, - autoSearch: true, - type: 'local', - minLength: 1 - }, - numberCell: { show: true, title: "#", width: 60 }, - freezeCols: 2, - - }); - - const groupModel = { - on: true, - dataIndx: ['customer'], - collapsed: [false], - merge: true, - showSummary: [true], - grandSummary: true, - summaryTitle: 'Subtotal:' - } - $(this.orderGridRef.el).pqGrid("groupOption", groupModel); + if (!this.orderGridRef.el) { + return; } - } - initializeDispatchGrid() { - if (!this.dispatchGridRef.el || this.state.dispatchData.length === 0) { - return; - } + // Destroy existing grid if it exists + if ($(this.orderGridRef.el).pqGrid('instance')) { + $(this.orderGridRef.el).pqGrid('destroy'); + } - if (typeof window.$ === 'undefined' || typeof window.pq === 'undefined') { - console.warn('jQuery or paramQuery not loaded'); - return; - } - const agg = pq.aggregate; + if (this.state.gridData.length === 0) { + $(this.orderGridRef.el).html('
No data available for the selected period
'); + return; + } + + if (typeof window.$ === 'undefined' || typeof window.pq === 'undefined') { + console.warn('jQuery or paramQuery not loaded'); + return; + } + + const columns = this.getOrderGridColumns(); + + // Define aggregate function for summary + const agg = pq.aggregate; agg.sum_ = function(arr, col) { return " " + agg.sum(arr, col).toFixed(2).toString(); }; - // -------------------------------------------------- - // 1. Collect unique invoice dates from query result - // -------------------------------------------------- - const dateSet = new Set(); - - this.state.dispatchData.forEach(row => { - (row.invoice_date_qty || []).forEach(d => { - dateSet.add(d.invoice_date); - }); - }); - - const dateColumns = Array.from(dateSet).sort(); - - // -------------------------------------------------- - // 2. Flatten date-wise quantities into row object - // -------------------------------------------------- - this.state.dispatchData.forEach(row => { - (row.invoice_date_qty || []).forEach(d => { - row[d.invoice_date] = d.qty; - }); - delete row.invoice_date_qty; - }); - - // -------------------------------------------------- - // 3. Base columns - // -------------------------------------------------- - const columns = this.getDispatchGridColumns(); - - // -------------------------------------------------- - // 4. Dynamic date columns from query - // -------------------------------------------------- - dateColumns.forEach(date => { - const [year, month, day] = date.split('-'); - columns.push({ - title: `${day}/${month}/${year}`, // day only - dataIndx: date, - width: 100, - align: "right", - dataType: "float", - summary: { type: "sum_" } - }); - }); - - // -------------------------------------------------- - // 5. Destroy existing grid (important) - // -------------------------------------------------- - const $grid = $(this.dispatchGridRef.el); - if ($grid.data("pqGrid")) { - $grid.pqGrid("destroy"); - } - - // -------------------------------------------------- - // 6. Initialize ParamQuery Grid - // -------------------------------------------------- - $grid.pqGrid({ - width: "100%", - height: 600, - title: "Dispatch Summary", - editable:false, - - dataModel: { - data: this.state.dispatchData, - location: "local", - sorting: "local", - paging: "local", - rPP: 20 - }, - - colModel: columns, - - numberCell: { - show: true, - title: "#", - width: 40 - }, - - freezeCols: 4, - summaryModel: { + $(this.orderGridRef.el).pqGrid({ + width: "100%", + height: 500, + title: "Customer Order Status", + editable: false, + dataModel: { + data: this.state.gridData, + location: "local", + sorting: "local", + paging: "local", + rPP: 50, + rPPOptions: [20, 50, 100, 200] + }, + summaryModel: { on: true, type: 'top', title: 'Grand Total:', - col: ['order_qty', 'invoiced_qty'] + col: ['order_qty', 'order_qty_kg', 'produced_qty', 'produced_qty_kg', 'dispatched_qty', 'dispatched_qty_kg', 'fg_qty', 'remaining_to_produce', 'remaining_dispatch_qty'] }, - - filterModel: { - on: true, - mode: "AND", - header: true, - autoSearch: true, - type: "local" - }, - - style: { - border: "1px solid #dee2e6", - borderRadius: "4px" - } - }); - const groupModel = { + colModel: columns, + selectionModel: { + type: "row", + mode: "single" + }, + filterModel: { on: true, - dataIndx: ['customer'], - collapsed: [false], - merge: true, - showSummary: [true,false], - grandSummary: true, - summaryTitle: 'Subtotal:' - + mode: "AND", + header: true, + autoSearch: true, + type: 'local', + minLength: 1 + }, + sortModel: { + on: true, + type: 'local' + }, + numberCell: { show: true, title: "#", width: 60 }, + freezeCols: 2, + columnBorders: true, + roundCorners: true, + stripeRows: true, + resizable: true, + style: { + border: '1px solid #dee2e6', + borderRadius: '4px' } - $grid.pqGrid("groupOption", groupModel); -} + }); + // Apply grouping + const groupModel = { + on: true, + dataIndx: ['customer'], + collapsed: [false], + merge: true, + showSummary: [true], + grandSummary: true, + summaryTitle: 'Subtotal:' + }; + $(this.orderGridRef.el).pqGrid("groupOption", groupModel); + } + + initializeDispatchGrid() { + if (!this.dispatchGridRef.el) { + return; + } + + // Destroy existing grid if it exists + if ($(this.dispatchGridRef.el).pqGrid('instance')) { + $(this.dispatchGridRef.el).pqGrid('destroy'); + } + + if (this.state.dispatchData.length === 0) { + $(this.dispatchGridRef.el).html('
No dispatch data available for the selected period
'); + return; + } + + if (typeof window.$ === 'undefined' || typeof window.pq === 'undefined') { + console.warn('jQuery or paramQuery not loaded'); + return; + } + + // Define aggregate function for summary + const agg = pq.aggregate; + agg.sum_ = function(arr, col) { + return " " + agg.sum(arr, col).toFixed(2).toString(); + }; + + // Get columns with dynamic date columns + const columns = this.getDispatchGridColumns(); + + $(this.dispatchGridRef.el).pqGrid({ + width: "100%", + height: 600, + title: `Dispatch Summary - ${this.getMonthYearLabel()}`, + editable: false, + dataModel: { + data: this.state.dispatchData, + location: "local", + sorting: "local", + paging: "local", + rPP: 20, + rPPOptions: [20, 50, 100, 200] + }, + colModel: columns, + numberCell: { + show: true, + title: "#", + width: 40 + }, + freezeCols: 4, + summaryModel: { + on: true, + type: 'top', + title: 'Grand Total:', + col: ['order_qty', 'order_qty_kg', 'invoiced_qty', 'invoiced_qty_kg', 'balance_qty'] + }, + filterModel: { + on: true, + mode: "AND", + header: true, + autoSearch: true, + type: "local" + }, + sortModel: { + on: true, + type: 'local' + }, + columnBorders: true, + roundCorners: true, + stripeRows: true, + resizable: true, + style: { + border: "1px solid #dee2e6", + borderRadius: "4px" + } + }); + + // Apply grouping + const groupModel = { + on: true, + dataIndx: ['customer'], + collapsed: [false], + merge: true, + showSummary: [true, false], + grandSummary: true, + summaryTitle: 'Subtotal:' + }; + $(this.dispatchGridRef.el).pqGrid("groupOption", groupModel); + } initializeRMGrid() { - if (this.rmGridRef.el && this.state.rmData.length > 0) { - const columns = this.getRMGridColumns(); - - if (typeof window.$ === 'undefined' || typeof window.pq === 'undefined') { - console.warn('jQuery or paramQuery not loaded'); - return; - } - - $(this.rmGridRef.el).pqGrid({ - width: "100%", - height: 500, - title: "Raw Material Availability", - dataModel: { - data: this.state.rmData, - location: "local", - sorting: "local", - paging: "local", - rPP: 30 - }, - colModel: columns, - numberCell: { show: true, title: "#", width: 30 }, - scrollModel: { autoFit: true }, - editable:false, - freezeCols: 1, - filterModel: { - on: true, - mode: "AND", - header: true, - autoSearch: true, - type: 'local' - }, - style: { - border: '1px solid #dee2e6', - borderRadius: '4px' - } - }); + if (!this.rmGridRef.el) { + return; } + + // Destroy existing grid if it exists + if ($(this.rmGridRef.el).pqGrid('instance')) { + $(this.rmGridRef.el).pqGrid('destroy'); + } + + if (this.state.rmData.length === 0) { + $(this.rmGridRef.el).html('
No raw material data available for the selected period
'); + return; + } + + if (typeof window.$ === 'undefined' || typeof window.pq === 'undefined') { + console.warn('jQuery or paramQuery not loaded'); + return; + } + + const columns = this.getRMGridColumns(); + + $(this.rmGridRef.el).pqGrid({ + width: "100%", + height: 500, + title: `Raw Material Availability - ${this.getMonthYearLabel()}`, + dataModel: { + data: this.state.rmData, + location: "local", + sorting: "local", + paging: "local", + rPP: 30, + rPPOptions: [20, 30, 50, 100] + }, + colModel: columns, + numberCell: { show: true, title: "#", width: 30 }, + scrollModel: { autoFit: true }, + editable: false, + freezeCols: 1, + filterModel: { + on: true, + mode: "AND", + header: true, + autoSearch: true, + type: 'local' + }, + sortModel: { + on: true, + type: 'local' + }, + columnBorders: true, + roundCorners: true, + stripeRows: true, + resizable: true, + style: { + border: '1px solid #dee2e6', + borderRadius: '4px' + } + }); } initializeDPRGrid() { - if (this.dprGridRef.el && this.state.dprData.length > 0) { - const columns = this.getDPRGridColumns(); - - if (typeof window.$ === 'undefined' || typeof window.pq === 'undefined') { - console.warn('jQuery or paramQuery not loaded'); - return; - } - - $(this.dprGridRef.el).pqGrid({ - width: "100%", - height: 500, - title: "Daily Production Report", - editable:false, - dataModel: { - data: this.state.dprData, - location: "local", - sorting: "local", - paging: "local", - rPP: 20 - }, - colModel: columns, - numberCell: { show: true, title: "#", width: 40 }, - scrollModel: { autoFit: true, horizontal: true }, - freezeCols: 2, - filterModel: { - on: true, - mode: "AND", - header: true, - autoSearch: true, - type: 'local' - }, - style: { - border: '1px solid #dee2e6', - borderRadius: '4px' - } - }); + if (!this.dprGridRef.el) { + return; } + + // Destroy existing grid if it exists + if ($(this.dprGridRef.el).pqGrid('instance')) { + $(this.dprGridRef.el).pqGrid('destroy'); + } + + if (this.state.dprData.length === 0) { + $(this.dprGridRef.el).html('
No DPR data available for the selected period
'); + return; + } + + if (typeof window.$ === 'undefined' || typeof window.pq === 'undefined') { + console.warn('jQuery or paramQuery not loaded'); + return; + } + + const columns = this.getDPRGridColumns(); + + // Define aggregate function for summary + const agg = pq.aggregate; + agg.sum_ = function(arr, col) { + return " " + agg.sum(arr, col).toFixed(2).toString(); + }; + + $(this.dprGridRef.el).pqGrid({ + width: "100%", + height: 500, + title: `Daily Production Report - ${this.getMonthYearLabel()}`, + editable: false, + dataModel: { + data: this.state.dprData || [], + location: "local", + sorting: "local", + paging: "local", + rPP: 20, + rPPOptions: [20, 50, 100, 200] + }, + colModel: columns, + numberCell: { show: true, title: "#", width: 40 }, + freezeCols: 4, + filterModel: { + on: true, + mode: "AND", + header: true, + autoSearch: true, + type: 'local' + }, + sortModel: { + on: true, + type: 'local' + }, + summaryModel: { + on: true, + type: 'top', + title: 'Grand Total:', + col: ['pending_order', 'current_month_order', 'total_order_qty', 'fg_qty', 'dispatch_qty', 'remaining_production'] + }, + columnBorders: true, + roundCorners: true, + stripeRows: true, + resizable: true, + style: { + border: '1px solid #dee2e6', + borderRadius: '4px' + } + }); + + // Apply grouping + const groupModel = { + on: true, + dataIndx: ['customer'], + collapsed: [false], + merge: true, + showSummary: [true, false], + grandSummary: true, + summaryTitle: 'Subtotal:' + }; + $(this.dprGridRef.el).pqGrid("groupOption", groupModel); } // ==================== @@ -729,7 +951,7 @@ export class CustomerOrderStatusGrid extends Component { return [ { title: "Customer", - width: 230, + width: 280, dataIndx: "customer", align: "left", dataType: "string", @@ -740,7 +962,7 @@ export class CustomerOrderStatusGrid extends Component { }, { title: "Product", - width: 280, + width: 300, dataIndx: "product", align: "left", dataType: "string", @@ -766,7 +988,8 @@ export class CustomerOrderStatusGrid extends Component { type: "sum", label: "Total:", render: (ui) => this.formatInteger(ui.data) - } + }, + style: { textAlign: 'right' } }, { title: "Kgs", @@ -781,7 +1004,8 @@ export class CustomerOrderStatusGrid extends Component { type: "sum", label: "Total:", render: (ui) => this.formatFloat(ui.data) - } + }, + style: { textAlign: 'right' } } ] }, @@ -794,12 +1018,12 @@ export class CustomerOrderStatusGrid extends Component { sortable: true, align: "right", render: (ui) => this.formatInteger(ui.cellData), - summary: { type: "sum", label: "Total:", render: (ui) => this.formatInteger(ui.data) - } + }, + style: { textAlign: 'right', backgroundColor: '#d4edda' } }, { title: "Produced Qty", @@ -818,7 +1042,8 @@ export class CustomerOrderStatusGrid extends Component { type: "sum", label: "Total:", render: (ui) => this.formatInteger(ui.data) - } + }, + style: { textAlign: 'right' } }, { title: "Kgs", @@ -833,7 +1058,8 @@ export class CustomerOrderStatusGrid extends Component { type: "sum", label: "Total:", render: (ui) => this.formatFloat(ui.data) - } + }, + style: { textAlign: 'right' } } ] }, @@ -850,12 +1076,12 @@ export class CustomerOrderStatusGrid extends Component { sortable: true, align: "right", render: (ui) => this.formatInteger(ui.cellData), - summary: { type: "sum", label: "Total:", render: (ui) => this.formatInteger(ui.data) - } + }, + style: { textAlign: 'right' } }, { title: "Kgs", @@ -866,12 +1092,12 @@ export class CustomerOrderStatusGrid extends Component { sortable: true, align: "right", render: (ui) => this.formatFloat(ui.cellData), - summary: { type: "sum", label: "Total:", render: (ui) => this.formatFloat(ui.data) - } + }, + style: { textAlign: 'right' } } ] }, @@ -888,7 +1114,8 @@ export class CustomerOrderStatusGrid extends Component { type: "sum", label: "Total:", render: (ui) => this.formatInteger(ui.data) - } + }, + style: { textAlign: 'right', backgroundColor: '#f8d7da' } }, { title: "Remaining Dispatch", @@ -899,12 +1126,12 @@ export class CustomerOrderStatusGrid extends Component { sortable: true, align: "right", render: (ui) => this.formatInteger(ui.cellData), - summary: { type: "sum", label: "Total:", render: (ui) => this.formatInteger(ui.data) - } + }, + style: { textAlign: 'right', backgroundColor: '#fff3cd' } }, { title: "Completion %", @@ -917,9 +1144,9 @@ export class CustomerOrderStatusGrid extends Component { render: (ui) => `${parseFloat(ui.cellData).toFixed(1)}%`, style: (ui) => { const value = parseFloat(ui.cellData) || 0; - if (value >= 100) return { backgroundColor: '#d4edda', color: '#155724', fontWeight: 'bold' }; - if (value >= 80) return { backgroundColor: '#fff3cd', color: '#856404', fontWeight: 'bold' }; - return { backgroundColor: '#f8d7da', color: '#721c24', fontWeight: 'bold' }; + if (value >= 100) return { backgroundColor: '#d4edda', color: '#155724', fontWeight: 'bold', textAlign: 'right' }; + if (value >= 80) return { backgroundColor: '#fff3cd', color: '#856404', fontWeight: 'bold', textAlign: 'right' }; + return { backgroundColor: '#f8d7da', color: '#721c24', fontWeight: 'bold', textAlign: 'right' }; }, summary: { type: "avg", @@ -930,35 +1157,26 @@ export class CustomerOrderStatusGrid extends Component { ]; } - getMonthDates(year, month) { - const dates = []; - const date = new Date(year, month - 1, 1); - - while (date.getMonth() === month - 1) { - dates.push(new Date(date)); - date.setDate(date.getDate() + 1); - } - return dates; - } - getDispatchGridColumns() { - return [ + const columns = [ { title: "Customer", - width: 220, + width: 280, dataIndx: "customer", dataType: "string", align: "left", frozen: true, + filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] }, style: { backgroundColor: '#fff3cd', fontWeight: 'bold' } }, { title: "Product", - width: 250, + width: 300, dataIndx: "product", dataType: "string", align: "left", frozen: true, + filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] }, style: { backgroundColor: '#fff3cd' } }, { @@ -977,7 +1195,8 @@ export class CustomerOrderStatusGrid extends Component { type: "sum", label: "Total:", render: (ui) => this.formatInteger(ui.data) - } + }, + style: { textAlign: 'right' } }, { title: "Kgs", @@ -992,13 +1211,14 @@ export class CustomerOrderStatusGrid extends Component { type: "sum", label: "Total:", render: (ui) => this.formatFloat(ui.data) - } + }, + style: { textAlign: 'right' } } ] }, { - title: "Dispatched Qty", - colModel: [ + title: "Total Dispatched", + colModel: [ { title: "Bags", width: 90, @@ -1008,12 +1228,12 @@ export class CustomerOrderStatusGrid extends Component { sortable: true, align: "right", render: (ui) => this.formatInteger(ui.cellData), - summary: { type: "sum", label: "Total:", render: (ui) => this.formatInteger(ui.data) - } + }, + style: { textAlign: 'right' } }, { title: "Kgs", @@ -1024,12 +1244,12 @@ export class CustomerOrderStatusGrid extends Component { sortable: true, align: "right", render: (ui) => this.formatFloat(ui.cellData), - summary: { type: "sum", label: "Total:", render: (ui) => this.formatFloat(ui.data) - } + }, + style: { textAlign: 'right' } } ] }, @@ -1041,10 +1261,11 @@ export class CustomerOrderStatusGrid extends Component { align: "right", render: (ui) => this.formatInteger(ui.cellData), summary: { - type: "sum", - label: "Total:", - render: (ui) => this.formatInteger(ui.data) - } + type: "sum", + label: "Total:", + render: (ui) => this.formatInteger(ui.data) + }, + style: { textAlign: 'right', backgroundColor: '#f8d7da' } }, { title: "Status", @@ -1053,21 +1274,30 @@ export class CustomerOrderStatusGrid extends Component { dataIndx: "dispatch_status", dataType: "string", align: "center", - render: (ui) => { - const status = ui.cellData || ''; // Default to empty string if undefined - if (!status) return ''; // Return empty if no status - + render: (ui) => { + const status = ui.cellData || ''; + if (!status) return ''; const color = status === 'Full' ? '#155724' : '#856404'; return `${status}`; }, - attr:ui => ({ - style: { - backgroundColor: ui.cellData > 0 ? '#d4edda' : '', - textAlign: 'right' - } - }) + style: { textAlign: 'center' } } ]; + + // Add dynamic date columns for daily dispatch quantities + if (this.state.selectedMonthYear && this.state.dispatchDateColumns) { + // Create a date columns section + const dateSection = { + title: "Daily Dispatch Quantities", + align: "center", + colModel: this.state.dispatchDateColumns + }; + + // Insert date columns before the Status column + columns.splice(columns.length - 1, 0, dateSection); + } + + return columns; } getRMGridColumns() { @@ -1082,10 +1312,9 @@ export class CustomerOrderStatusGrid extends Component { style: { backgroundColor: '#f0f8ff', fontWeight: 'bold' }, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, - { title: "Required Qty", - width: 70, + width: 100, dataIndx: "required_qty", dataType: "float", align: "right", @@ -1094,7 +1323,7 @@ export class CustomerOrderStatusGrid extends Component { }, { title: "Available Qty", - width: 70, + width: 100, dataIndx: "available_qty", dataType: "float", align: "right", @@ -1103,7 +1332,7 @@ export class CustomerOrderStatusGrid extends Component { }, { title: "Shortage Qty", - width: 70, + width: 100, dataIndx: "shortage_qty", dataType: "float", align: "right", @@ -1121,103 +1350,118 @@ export class CustomerOrderStatusGrid extends Component { width: 60, dataIndx: "uom_name", dataType: "string", - align: "left", + align: "center", style: { textAlign: 'center' } }, { title: "Status", - width: 70, + width: 80, dataIndx: "rm_status", dataType: "string", align: "center", render: (ui) => { const status = ui.cellData; + if (!status) return ''; const color = status === 'Available' ? '#155724' : '#721c24'; return `${status}`; }, - style: (ui) => ({ - backgroundColor: ui.cellData === 'Available' ? '#d4edda' : '#f8d7da', - textAlign: 'center' - }) + style: { textAlign: 'center' } } ]; } getDPRGridColumns() { - const dayColumns = []; - - // Add day columns for last 31 days - for (let i = 1; i <= 31; i++) { - const dayLabel = this.state.dprSummary.dayColumns?.[`day${i}`]?.label || `Day ${i}`; - dayColumns.push({ - title: dayLabel, - width: 60, - dataIndx: `day${i}`, - dataType: "float", - align: "right", - render: (ui) => this.formatInteger(ui.cellData), - style: { - backgroundColor: '#f0f8ff', - fontSize: '11px', - textAlign: 'right' - } - }); - } - - return [ + // Base static columns + const baseColumns = [ + { + title: "Customer", + width: 280, + dataIndx: "customer", + dataType: "string", + align: "left", + frozen: true, + filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] }, + style: { backgroundColor: '#fff3cd', fontWeight: 'bold' } + }, { title: "Product", - width: 120, + width: 300, dataIndx: "product", dataType: "string", align: "left", frozen: true, + filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] }, style: { backgroundColor: '#e8f4f8', fontWeight: 'bold' } }, { title: "Pending Order", - width: 80, + width: 90, dataIndx: "pending_order", dataType: "float", align: "right", render: (ui) => this.formatInteger(ui.cellData), frozen: true, - style: { backgroundColor: '#fff3cd', textAlign: 'right' } + style: { backgroundColor: '#fff3cd', textAlign: 'right' }, + summary: { + type: "sum", + label: "Total:", + render: (ui) => this.formatInteger(ui.data) + } }, { title: "Current Month Order", - width: 80, + width: 90, dataIndx: "current_month_order", dataType: "float", align: "right", render: (ui) => this.formatInteger(ui.cellData), - style: { backgroundColor: '#e3f2fd', textAlign: 'right' } + style: { backgroundColor: '#e3f2fd', textAlign: 'right' }, + summary: { + type: "sum", + label: "Total:", + render: (ui) => this.formatInteger(ui.data) + } }, { title: "Total Order Qty", - width: 80, + width: 90, dataIndx: "total_order_qty", dataType: "float", align: "right", render: (ui) => this.formatInteger(ui.cellData), + summary: { + type: "sum", + label: "Total:", + render: (ui) => this.formatInteger(ui.data) + }, style: { backgroundColor: '#f8f9fa', fontWeight: 'bold', textAlign: 'right' } }, { title: "FG Available", - width: 80, - dataIndx: "fg_available", + width: 90, + dataIndx: "fg_qty", dataType: "float", align: "right", render: (ui) => this.formatInteger(ui.cellData), + summary: { + type: "sum", + label: "Total:", + render: (ui) => this.formatInteger(ui.data) + }, style: { backgroundColor: '#d4edda', textAlign: 'right' } }, { title: "Dispatch Qty", - width: 80, + width: 90, dataIndx: "dispatch_qty", dataType: "float", align: "right", render: (ui) => this.formatInteger(ui.cellData), + summary: { + type: "sum", + label: "Total:", + render: (ui) => this.formatInteger(ui.data) + }, style: { backgroundColor: '#e3f2fd', textAlign: 'right' } }, { @@ -1227,6 +1471,11 @@ export class CustomerOrderStatusGrid extends Component { dataType: "float", align: "right", render: (ui) => this.formatInteger(ui.cellData), + summary: { + type: "sum", + label: "Total:", + render: (ui) => this.formatInteger(ui.data) + }, style: (ui) => { const value = parseFloat(ui.cellData) || 0; return { @@ -1235,13 +1484,26 @@ export class CustomerOrderStatusGrid extends Component { textAlign: 'right' }; } - }, - ...dayColumns + } ]; + + // Add dynamic date columns for daily production + if (this.state.selectedMonthYear && this.state.dprDateColumns) { + // Create a date columns section + const dateSection = { + title: "Daily Production Quantities", + align: "center", + colModel: this.state.dprDateColumns + }; + + return [...baseColumns, dateSection]; + } + + return baseColumns; } // ==================== - // HELPER METHODS + // REFRESH METHODS // ==================== async loadAllData() { @@ -1251,6 +1513,10 @@ export class CustomerOrderStatusGrid extends Component { this.loadRMAvailability(), this.loadDPRData() ]); + } + + async refreshAllData() { + await this.loadAllData(); this.notification.add("All data refreshed successfully", { type: "success", title: "Refresh" @@ -1263,7 +1529,6 @@ export class CustomerOrderStatusGrid extends Component { type: "success", title: "Refresh" }); - this.initializeOrderGrid(); } async refreshDispatchSummary() { @@ -1272,7 +1537,6 @@ export class CustomerOrderStatusGrid extends Component { type: "success", title: "Refresh" }); - this.initializeDispatchGrid(); } async refreshRMAvailability() { @@ -1281,7 +1545,6 @@ export class CustomerOrderStatusGrid extends Component { type: "success", title: "Refresh" }); - this.initializeRMGrid(); } async refreshDPRData() { @@ -1290,120 +1553,426 @@ export class CustomerOrderStatusGrid extends Component { type: "success", title: "Refresh" }); - this.initializeDPRGrid(); } - formatDate(date) { - if (!date) return "N/A"; - return date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }) + ' ' + date.toLocaleDateString(); + // ==================== + // EXPORT METHODS + // ==================== + +// Replace your exportGrid method with this: +// ==================== +// EXPORT METHODS - FIXED FOR DATA EXPORT +// ==================== + +// ==================== +// EXPORT METHODS - WITH MERGED CELLS FOR GROUP HEADERS +// ==================== + +exportGrid(ev, type) { + ev.preventDefault(); + ev.stopPropagation(); + + let gridData; + let columns; + let sheetName; + let filename; + + switch (type) { + case 'order': + gridData = this.state.gridData; + columns = this.getOrderGridColumns(); + sheetName = 'Customer Order Status'; + filename = `Customer_Order_Status_${this.getMonthYearLabel().replace(/ /g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`; + break; + case 'dispatch': + gridData = this.state.dispatchData; + columns = this.getDispatchGridColumns(); + sheetName = 'Dispatch Summary'; + filename = `Dispatch_Summary_${this.getMonthYearLabel().replace(/ /g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`; + break; + case 'rm': + gridData = this.state.rmData; + columns = this.getRMGridColumns(); + sheetName = 'Raw Material Availability'; + filename = `RM_Availability_${this.getMonthYearLabel().replace(/ /g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`; + break; + case 'dpr': + gridData = this.state.dprData; + columns = this.getDPRGridColumns(); + sheetName = 'Daily Production Report'; + filename = `DPR_${this.getMonthYearLabel().replace(/ /g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`; + break; + default: + return; } - getCompletionRateClass(rate) { - if (rate >= 100) return 'badge-success'; - if (rate >= 80) return 'badge-warning'; - return 'badge-danger'; + if (!gridData || gridData.length === 0) { + this.notification.add("No data to export", { + type: "warning", + title: "Export Error" + }); + return; } - async exportGrid(type) { + try { + // Generate HTML table with merged headers + const table = this.generateMergedHeaderTable(gridData, columns, sheetName); - let gridRef; - let filename; + // Convert to Excel + this.exportToExcel(table, filename, sheetName); - switch(type) { - case 'order': - gridRef = this.orderGridRef; - filename = `Customer_Order_Status_${new Date().toISOString().split('T')[0]}.xlsx`; - break; - case 'dispatch': - gridRef = this.dispatchGridRef; - filename = `Dispatch_Summary_${new Date().toISOString().split('T')[0]}.xlsx`; - break; - case 'rm': - gridRef = this.rmGridRef; - filename = `RM_Availability_${new Date().toISOString().split('T')[0]}.xlsx`; - break; - case 'dpr': - gridRef = this.dprGridRef; - filename = `DPR_${new Date().toISOString().split('T')[0]}.xlsx`; - break; - default: - return; - } + } catch (error) { + console.error("Export error:", error); + this.notification.add("Failed to export data", { + type: "danger", + title: "Export Error" + }); + } +} - const grid = gridRef?.el ? $(gridRef.el) : null; - if (grid && grid.pqGrid) { - const blob = grid.pqGrid("exportData", { - format: 'xlsx', - render: true, - title: filename.replace('.xlsx', '') - }); +// Generate HTML table with merged column headers +generateMergedHeaderTable(data, columns, sheetName) { + // First, analyze column structure to determine merge ranges + const headerRows = this.buildHeaderRows(columns); + const flatColumns = this.flattenColumns(columns); - if (blob) { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + let html = ''; - this.notification.add(`Export completed: ${filename}`, { - type: "success", - title: "Export" - }); + // ==================== HEADER SECTION WITH MERGED CELLS ==================== + html += ''; + + // Generate multi-row header with merged cells + for (let rowIdx = 0; rowIdx < headerRows.length; rowIdx++) { + html += ''; + const row = headerRows[rowIdx]; + + for (let colIdx = 0; colIdx < row.length; colIdx++) { + const cell = row[colIdx]; + if (cell) { + // Skip if this cell was already merged (null in the array) + if (cell.colspan === 0) continue; + + let colspan = cell.colspan || 1; + let rowspan = cell.rowspan || 1; + + html += ``; } } + html += ''; + } + html += ''; + + // ==================== BODY SECTION ==================== + html += ''; + + // Data rows + data.forEach((row, index) => { + // Add zebra striping + const bgColor = index % 2 === 0 ? '#FFFFFF' : '#F8F9FA'; + html += ''; + + flatColumns.forEach(col => { + let value = row[col.dataIndx]; + let displayValue = ''; + + // Format value based on data type + if (value === null || value === undefined || value === '' || value === 0) { + displayValue = ''; + } else if (col.dataType === 'float') { + displayValue = parseFloat(value).toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + } else if (col.dataType === 'integer') { + displayValue = parseInt(value).toLocaleString('en-US'); + } else { + displayValue = value.toString(); + } + + const align = col.dataType === 'float' || col.dataType === 'integer' ? 'right' : 'left'; + html += ``; + }); + html += ''; + }); + + // ==================== SUMMARY SECTION WITH MERGED CELL ==================== + html += ''; + + // Calculate totals for numeric columns + const totals = {}; + flatColumns.forEach(col => { + if (col.dataType === 'float' || col.dataType === 'integer') { + totals[col.dataIndx] = 0; + data.forEach(row => { + totals[col.dataIndx] += parseFloat(row[col.dataIndx]) || 0; + }); + } + }); + + // Grand Total row with merged first cell + html += ''; + html += ``; + + // Add totals for remaining columns + for (let i = 1; i < flatColumns.length; i++) { + const col = flatColumns[i]; + if (col.dataType === 'float' || col.dataType === 'integer') { + let total = totals[col.dataIndx] || 0; + let formattedTotal = col.dataType === 'float' + ? total.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + : total.toLocaleString('en-US'); + html += ``; + } else { + html += ``; + } + } + html += ''; + + html += ''; + html += '
${cell.title}
${displayValue}
GRAND TOTAL${formattedTotal}
'; + + return html; +} + +// Build multi-row header structure with merge information +buildHeaderRows(columns) { + const headerRows = []; + + // First, determine the depth of the header + const getDepth = (cols) => { + let maxDepth = 1; + cols.forEach(col => { + if (col.colModel) { + maxDepth = Math.max(maxDepth, 1 + getDepth(col.colModel)); + } + }); + return maxDepth; + }; + + const depth = getDepth(columns); + + // Initialize header rows + for (let i = 0; i < depth; i++) { + headerRows.push([]); } + // Recursively build header structure + const buildRow = (cols, row, colOffset, currentDepth) => { + let offset = colOffset; + + cols.forEach(col => { + if (col.colModel) { + // Group column - calculate total colspan + let totalCols = 0; + const countCols = (model) => { + let count = 0; + model.forEach(c => { + if (c.colModel) { + count += countCols(c.colModel); + } else { + count++; + } + }); + return count; + }; + + const colspan = countCols(col.colModel); + + // Add to current row + if (!headerRows[currentDepth][offset]) { + headerRows[currentDepth][offset] = { + title: col.title, + colspan: colspan, + rowspan: 1 + }; + } + + // Process child columns + buildRow(col.colModel, headerRows, offset, currentDepth + 1); + offset += colspan; + } else { + // Leaf column + headerRows[currentDepth][offset] = { + title: col.title, + colspan: 1, + rowspan: depth - currentDepth + }; + + // Fill lower rows with null (to be skipped during rendering) + for (let r = currentDepth + 1; r < depth; r++) { + headerRows[r][offset] = { title: null, colspan: 0 }; + } + offset++; + } + }); + + return offset; + }; + + buildRow(columns, headerRows, 0, 0); + + return headerRows; +} + +// Flatten nested columns +flattenColumns(columns) { + const flatColumns = []; + + const flatten = (cols, parentTitle = '') => { + cols.forEach(col => { + if (col.colModel) { + flatten(col.colModel, parentTitle ? `${parentTitle} - ${col.title}` : col.title); + } else { + flatColumns.push({ + dataIndx: col.dataIndx, + title: parentTitle ? `${parentTitle} - ${col.title}` : col.title, + width: col.dataIndx === 'product'? 600 : (col.width || 100), + align: col.align || 'left', + dataType: col.dataType || 'string' + }); + } + }); + }; + + flatten(columns); + return flatColumns; +} + +// Export to Excel +exportToExcel(htmlTable, filename, sheetName) { + const excelContent = ` + + + + + + + + + +

${sheetName}

+

+ ${this.getMonthYearLabel()} | Exported on: ${new Date().toLocaleString()} +

+ ${htmlTable} +

+ * All quantities are in bags unless specified otherwise +

+ + + `; + + const blob = new Blob([excelContent], { + type: 'application/vnd.ms-excel' + }); + + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 100); + + this.notification.add(`Export completed: ${filename}`, { + type: "success", + title: "Export" + }); +} + +// Helper methods for export buttons +exportOrderGrid(ev) { + this.exportGrid(ev, 'order'); +} + +exportDispatchGrid(ev) { + this.exportGrid(ev, 'dispatch'); +} + +exportRMGrid(ev) { + this.exportGrid(ev, 'rm'); +} + +exportDPRGrid(ev) { + this.exportGrid(ev, 'dpr'); +} + + // ==================== + // TAB EVENT HANDLER + // ==================== + onTabShow(event) { - const target = event.currentTarget || event.target; - const tabId = target.getAttribute('data-bs-target').replace('#', ''); - switch(tabId) { + const target = event.currentTarget || event.target; + const tabId = target.getAttribute('data-bs-target')?.replace('#', '') || + target.getAttribute('href')?.replace('#', '') || + 'order-content'; + + // Update active tab + this.state.activeTab = tabId.replace('-content', ''); + + switch (tabId) { case 'order-content': if (this.state.gridData.length > 0) { - setTimeout(() => { - if (this.orderGridRef.el && !$(this.orderGridRef.el).pqGrid) { - this.initializeOrderGrid(); - } - }, 100); + setTimeout(() => this.initializeOrderGrid(), 100); + } else { + this.loadGridData(); } break; case 'dispatch-content': - if (this.state.dispatchData.length === 0) { - this.loadDispatchSummary(); + if (this.state.dispatchData.length > 0) { + setTimeout(() => this.initializeDispatchGrid(), 100); } else { - setTimeout(() => { - if (this.dispatchGridRef.el && !$(this.dispatchGridRef.el).pqGrid) { - this.initializeDispatchGrid(); - } - }, 100); + this.loadDispatchSummary(); } break; case 'rm-content': - if (this.state.rmData.length === 0) { - this.loadRMAvailability(); + if (this.state.rmData.length > 0) { + setTimeout(() => this.initializeRMGrid(), 100); } else { - setTimeout(() => { - if (this.rmGridRef.el && !$(this.rmGridRef.el).pqGrid) { - this.initializeRMGrid(); - } - }, 100); + this.loadRMAvailability(); } break; case 'dpr-content': - if (this.state.dprData.length === 0) { - this.loadDPRData(); + if (this.state.dprData.length > 0) { + setTimeout(() => this.initializeDPRGrid(), 100); } else { - setTimeout(() => { - if (this.dprGridRef.el && !$(this.dprGridRef.el).pqGrid) { - this.initializeDPRGrid(); - } - }, 100); + this.loadDPRData(); } break; } diff --git a/custom_addons/customer_orders/static/src/xml/dashboard.xml b/custom_addons/customer_orders/static/src/xml/dashboard.xml index f47ea02ba..85f15c0a1 100644 --- a/custom_addons/customer_orders/static/src/xml/dashboard.xml +++ b/custom_addons/customer_orders/static/src/xml/dashboard.xml @@ -1,6 +1,5 @@ -
@@ -8,6 +7,7 @@
Customer Order Status Dashboard
+
@@ -16,15 +16,18 @@ - +
-
+ +
Total Orders: @@ -40,8 +43,7 @@
Production Rate: - + %
@@ -59,60 +61,68 @@
@@ -181,10 +193,12 @@
-
+
@@ -217,7 +231,7 @@ -
@@ -239,10 +253,12 @@
-
+
@@ -275,7 +291,7 @@ -
@@ -297,10 +313,12 @@
-
+
@@ -333,7 +351,7 @@ -
@@ -343,17 +361,6 @@

No DPR data available

-
-
Formulas:
-
-
- Total Order Qty = Pending Order + Present Month Order -
-
- Remaining to Produce = Total Order Qty – Closing FG -
-
-
@@ -438,7 +445,7 @@ padding: 5px 10px; min-width: 60px; text-align: center; - color : white; + color: white; } .refresh-btn { @@ -762,76 +769,48 @@ } /* Simple Filter Styles - Right side */ -.simple-filter { - margin-top: 15px; - padding: 10px 0; -} + .simple-filter { + margin-top: 15px; + padding: 10px 0; + } -/* Position filter on right side */ -.col-auto.ms-auto { - margin-left: auto !important; -} + /* Position filter on right side */ + .col-auto.ms-auto { + margin-left: auto !important; + } -.input-group-sm { - width: 160px; -} + .input-group-sm { + width: 160px; + } -.input-group-sm .form-control { - background: white; - border: 1px solid rgba(255, 255, 255, 0.3); - color: #495057; -} + .input-group-sm .form-control { + background: white; + border: 1px solid rgba(255, 255, 255, 0.3); + color: #495057; + } -.input-group-sm .input-group-text { - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); - color: white; -} + .input-group-sm .input-group-text { + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + } -/* Hover effects */ -.input-group-sm:hover .input-group-text { - background: rgba(255, 255, 255, 0.3); -} + /* Hover effects */ + .input-group-sm:hover .input-group-text { + background: rgba(255, 255, 255, 0.3); + } -/* Responsive */ -@media (max-width: 768px) { - .col-auto.ms-auto { - margin-left: 0 !important; - margin-top: 10px; - } + /* Responsive */ + @media (max-width: 768px) { + .col-auto.ms-auto { + margin-left: 0 !important; + margin-top: 10px; + } - .simple-filter { - text-align: center; - } -} - - /* ParamQuery Grid Customization */ - - - - - - - - - - - - - - - - - - - - - - - - - - + .simple-filter { + text-align: center; + } + } \ No newline at end of file