diff --git a/custom_addons/dashboard/__manifest__.py b/custom_addons/dashboard/__manifest__.py index f7141f8d8..7fbec4d00 100644 --- a/custom_addons/dashboard/__manifest__.py +++ b/custom_addons/dashboard/__manifest__.py @@ -16,7 +16,11 @@ ('include', 'web_grid._assets_pqgrid'), # Internal module JS and XML files (ensure correct paths within 'static/src') 'dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.js', + 'dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.js', + 'dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.js', 'dashboard/static/src/components/pqgrid_dashboard/pqgrid_stock_dashboard.xml', + 'dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.xml', + 'dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.xml', ], diff --git a/custom_addons/dashboard/controllers/sale_margin_excel.py b/custom_addons/dashboard/controllers/sale_margin_excel.py index 27ab0b093..8ae186b8e 100644 --- a/custom_addons/dashboard/controllers/sale_margin_excel.py +++ b/custom_addons/dashboard/controllers/sale_margin_excel.py @@ -38,7 +38,7 @@ class SaleMarginController(http.Controller): 'bg_color': '#366092', 'border': 1 }) text_fmt = workbook.add_format({ - 'font_size': 10, 'align': 'left', 'valign': 'vcenter', 'border': 1 + 'font_size': 10, 'align': 'left', 'valign': 'vcenter', 'border': 1,'text_wrap': False }) float_fmt = workbook.add_format({ 'font_size': 10, 'align': 'right', 'valign': 'vcenter', diff --git a/custom_addons/dashboard/models/stock_dashboard.py b/custom_addons/dashboard/models/stock_dashboard.py index 9a717e7ea..7d3fbc397 100644 --- a/custom_addons/dashboard/models/stock_dashboard.py +++ b/custom_addons/dashboard/models/stock_dashboard.py @@ -199,7 +199,7 @@ class SamashtiDashboard(models.AbstractModel): # Combine invoice names and dates if order.invoice_ids: invoice = ', '.join(inv.name for inv in order.invoice_ids) - date = ', '.join(str(inv.invoice_date) for inv in order.invoice_ids if inv.invoice_date) + date = ', '.join(str(inv.invoice_date.strftime('%d-%m-%Y')) for inv in order.invoice_ids if inv.invoice_date) else: invoice = "N/A" date = "No invoices" @@ -228,5 +228,50 @@ class SamashtiDashboard(models.AbstractModel): result['target'] = 'self', return result + @api.model + def get_consumption_data(self, from_date, to_date): + from_date_obj = datetime.strptime(from_date, '%Y-%m-%d').date() + to_date_obj = datetime.strptime(to_date, '%Y-%m-%d').date() + + # Get user's timezone + user_tz = self.env.user.tz or 'UTC' + local_tz = pytz.timezone(user_tz) + + # Convert local datetime to UTC + from_local = local_tz.localize(datetime.combine(from_date_obj, time.min)) + to_local = local_tz.localize(datetime.combine(to_date_obj, time.max)) + from_utc = from_local.astimezone(pytz.UTC) + to_utc = to_local.astimezone(pytz.UTC) + + # Convert to string in Odoo datetime format + fromDate = "'"+str(fields.Datetime.to_string(from_utc))+"'" + toDate = "'"+str(fields.Datetime.to_string(to_utc))+"'" + + mo_ids = self.env['mrp.production'].search([ + ('state', '=', 'done'), + ('date_start', '>=', fromDate), + ('date_start', '<=', toDate) + ]) + move_ids = mo_ids.move_raw_ids + mo_ids.move_finished_ids + mo_ids.scrap_ids.move_ids + stock_layer_ids = move_ids.filtered(lambda x:x.location_id.usage == 'production' or x.location_dest_id.usage == 'production').stock_valuation_layer_ids + data = [] + for l in stock_layer_ids: + mo = self.env['mrp.production'].search([('name', 'ilike', l.reference),('state','=','done')]) + product_tags = ', '.join( + tag.name for tag in l.mapped('product_id.product_tag_ids') + ) + data.append({ + 'product_code': l.product_id.default_code, + 'product_name': l.product_id.display_name, + 'date':mo.date_start.strftime('%d-%m-%Y'), + 'tags':product_tags, + 'uom': l.uom_id.name, + 'quantity':l.quantity, + 'value':l.value, + 'reference':l.reference + + }) + return data + diff --git a/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.js b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.js new file mode 100644 index 000000000..08fe37692 --- /dev/null +++ b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.js @@ -0,0 +1,421 @@ +/** @odoo-module **/ +import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; +import { standardActionServiceProps } from "@web/webclient/actions/action_service"; + +export class ConsumptionDashboard extends Component { + static props = { + ...standardActionServiceProps, + }; + + static template = "ConsumptionDashboard"; + + setup() { + this.orm = useService("orm"); + this.gridRef = useRef("gridContainer"); + this.notification = useService("notification"); + + this.state = useState({ + rows: [], + fromDate: "", + toDate: "" + }); + + 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; + } + } + + initializeDates() { + const today = new Date(); + const firstDay = new Date(today.getFullYear(), today.getMonth(), 1); + + const formatLocalDate = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + + this.state.fromDate = formatLocalDate(firstDay); + this.state.toDate = formatLocalDate(today); + } + + initDatePickers() { + const self = this; + $("#fromDate").datepicker({ + dateFormat: "yy-mm-dd", + defaultDate: this.state.fromDate, + onSelect(dateText) { + self.state.fromDate = dateText; + }, + }).datepicker("setDate", this.state.fromDate); + + $("#toDate").datepicker({ + dateFormat: "yy-mm-dd", + defaultDate: this.state.toDate, + onSelect(dateText) { + self.state.toDate = dateText; + }, + }).datepicker("setDate", this.state.toDate); + } + + async loadGridData() { + try { + const records = await this.orm.call("samashti.board", "get_consumption_data", [ + this.state.fromDate, + this.state.toDate, + ]); + this.state.rows = records || []; + this.renderGrid(); + } catch (error) { + console.error("Error loading consumption data:", error); + this.notification.add("Failed to load consumption data", { type: "danger" }); + } + } + + getProductPrefix(row) { + // Check for 'laminate' in product name (case insensitive) + if (row.product_name && row.product_name.toLowerCase().includes('laminate')) { + return 'Laminate'; + } + // Check for 'Outer bag' in product name (case insensitive) + if (row.product_name && row.product_name.toLowerCase().includes('outer bag')) { + return 'Outer bag'; + } + // Default to first 2 characters of product code + if (row.product_code && row.product_code.length >= 2) { + return row.product_code.substring(0, 2).toUpperCase(); + } + // Fallback if no product code + return 'OTHER'; + } + + processConsumptionData() { + const po_rows = this.state.rows.filter(i => i.value > 0); + const neg_rows = this.state.rows.filter(i => i.value < 0); + + // Group negative rows by reference and product prefix + const consumptionByRefAndPrefix = {}; + + neg_rows.forEach(row => { + if (row.product_code && row.reference) { + const prefix = this.getProductPrefix(row); + const reference = row.reference; + + // Initialize if not exists + if (!consumptionByRefAndPrefix[reference]) { + consumptionByRefAndPrefix[reference] = {}; + } + if (!consumptionByRefAndPrefix[reference][prefix]) { + consumptionByRefAndPrefix[reference][prefix] = 0; + } + + // Sum quantities (use absolute value since they're negative) + consumptionByRefAndPrefix[reference][prefix] += Math.abs(row.value || 0); + } + }); + + // Get all unique prefixes for column headers + const allPrefixes = [...new Set(neg_rows + .filter(row => row.product_code) + .map(row => this.getProductPrefix(row)) + )]; + + // Add consumption data to po_rows based on reference + po_rows.forEach(po_row => { + const reference = po_row.reference; + const consumptionData = consumptionByRefAndPrefix[reference] || {}; + + // Add each prefix field to the po_row + allPrefixes.forEach(prefix => { + po_row[prefix] = consumptionData[prefix] || 0; + }); + }); + + return { + data: po_rows, + prefixes: allPrefixes + }; + } + + async getColumns() { + const { prefixes } = this.processConsumptionData(); + + const baseColumns = [ + { + title: "Product Code", + dataIndx: "product_code", + width: 120, + filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } + }, + { title: "Date", dataIndx: "date", width: 150 }, + + { + title: "Product Name", + dataIndx: "product_name", + width: 250, + filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } + }, + { + title: "Tags", + dataIndx: "tags", + width: 150, + filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } + }, + { + title: "Reference", + dataIndx: "reference", + width: 150, + filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } + }, + { + title: "Quantity", + dataIndx: "quantity", + width: 90, + dataType: "float", + format: "#,###.00", + align: "right", + summary: { type: "sum" } + }, + { + title: "UOM", + dataIndx: "uom", + width: 80 + }, + { + title: "Value", + dataIndx: "value", + width: 120, + dataType: "float", + format: "#,###.00", + align: "right", + summary: { type: "sum" } + } + ]; + + const getColumnPriority = (prefix) => { + const priorityMap = { + 'Laminate': 1, + 'Outer bag': 2, + 'RM':3, + 'PM':4, + 'ST':5, + 'GS':6, + 'PR':7 + }; + + // Check for numeric prefixes (like ST, AB, etc.) +// if (/^[A-Z]{2}$/.test(prefix)) { +// return 100; // Medium priority for 2-letter codes +// } + + return priorityMap[prefix] || 1000; // Default low priority + }; + + // Sort prefixes by priority, then alphabetically + const sortedPrefixes = prefixes.sort((a, b) => { + const priorityA = getColumnPriority(a); + const priorityB = getColumnPriority(b); + + if (priorityA !== priorityB) { + return priorityA - priorityB; + } + + // Same priority, sort alphabetically + return a.localeCompare(b); + }); + + // Create dynamic columns + const dynamicColumns = sortedPrefixes.map(prefix => ({ + dataIndx: prefix, + title: prefix+" ₹" , + width: 120, + align: "right", + dataType: "float", + format: "#,###.00", + summary: { type: "sum" }, + render: function(ui) { + const value = ui.cellData; +// if (value > 0) { +// return `${value}`; +// } + return value ? value.toFixed(2) : "0"; + } + })); + + return [...baseColumns, ...dynamicColumns]; + } + + async renderGrid() { + const { data } = this.processConsumptionData(); + const columns = await this.getColumns(); + + const gridOptions = { + selectionModel: { type: "row" }, + width: "100%", + height: "100%", + editable: false, + stripeRows: true, + filterModel: { + on: true, + mode: "AND", + header: true, + autoSearch: true, + type: 'local', + minLength: 1 + }, + dataModel: { + data: data, + 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 #ff6b35; border-radius: 6px; background: linear-gradient(135deg, #ff6b35, #f7931e); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(255, 107, 53, 0.3);"', + listener: () => this.exportGrid() + }, + { + type: 'button', + icon: 'ui-icon-print', + label: 'Print', + attr: 'style="padding: 8px 16px; border: 1px solid #ff6b35; border-radius: 6px; background: linear-gradient(135deg, #ff6b35, #f7931e); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(255, 107, 53, 0.3);"', + listener: function () { + var exportHtml = this.exportData({ + title: 'Consolidated Consumption 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, + } + }; + + // Destroy existing grid if any + const existingGrid = $(this.gridRef.el).pqGrid(); + if (existingGrid.length) { + existingGrid.pqGrid("destroy"); + } + + $(this.gridRef.el) + .css({ height: '600px', width: '100%' }) + .pqGrid(gridOptions); + + // Add grouping for better analysis + const groupModel = { + on: true, + dataIndx: ['tags'], + 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"); + } + + exportGrid() { + const format_ex = $("#export_format").val(); + const grid = $(this.gridRef.el); + + switch (format_ex) { + case 'xlsx': + const blobXlsx = grid.pqGrid("exportData", { + format: 'xlsx', + render: true + }); + saveAs(blobXlsx, "Consumption_Report.xlsx"); + break; + + case 'csv': + const blobCsv = grid.pqGrid("exportData", { + format: 'csv', + render: true + }); + if (typeof blobCsv === 'string') { + saveAs(new Blob([blobCsv], { type: 'text/csv' }), "Consumption_Report.csv"); + } else { + saveAs(blobCsv, "Consumption_Report.csv"); + } + break; + + case 'htm': + case 'html': + const blobHtml = grid.pqGrid("exportData", { + format: 'htm', + render: true + }); + if (typeof blobHtml === 'string') { + saveAs(new Blob([blobHtml], { type: 'text/html' }), "Consumption_Report.html"); + } else { + saveAs(blobHtml, "Consumption_Report.html"); + } + break; + + case 'json': + const blobJson = grid.pqGrid("exportData", { + format: 'json', + render: true + }); + if (typeof blobJson === 'string') { + saveAs(new Blob([blobJson], { type: 'application/json' }), "Consumption_Report.json"); + } else { + saveAs(blobJson, "Consumption_Report.json"); + } + break; + + default: + alert('Unsupported format: ' + format_ex); + } + } +} + +registry.category("actions").add("ConsumptionDashboard", ConsumptionDashboard); \ No newline at end of file diff --git a/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.xml b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.xml new file mode 100644 index 000000000..ea1e531c0 --- /dev/null +++ b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.xml @@ -0,0 +1,115 @@ + + + +
+ +
+
+
+ +
+
+
+ +
+
+

Consolidated Consumption Report

+

+ + + + +

+
+
+
+ + +
+
+
+
+ + + + +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ Consumption Analysis + +
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.js b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.js new file mode 100644 index 000000000..1e8871797 --- /dev/null +++ b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.js @@ -0,0 +1,243 @@ +/** @odoo-module **/ +import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; +import { standardActionServiceProps } from "@web/webclient/actions/action_service"; + + + +export class SaleDashboard extends Component { + static props = { + ...standardActionServiceProps, + }; + static template = "SaleDashboard"; + + setup() { + this.orm = useService("orm"); + this.gridSaleContainer = useRef("gridSaleContainer"); + this.notification = useService("notification"); + this.action = useService("action"); + + this.state = useState({ + sale_rows: [], + saleFromDate: "", + saleToDate: "" + }); + + onWillStart(async () => { + await this.loadDependencies(); + this.initializeDates(); + await this.loadSaleData(); + }); + + onMounted(() => { + this.initDatePickers(); + if (this.gridSaleContainer.el) { + this.renderSaleGrid(); + } + }); + } + + 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; + } + } + + initializeDates() { + const today = new Date(); + const firstDay = new Date(today.getFullYear(), today.getMonth(), 1); + + const formatLocalDate = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + + this.state.saleFromDate = formatLocalDate(firstDay); + this.state.saleToDate = formatLocalDate(today); + } + + initDatePickers() { + const self = this; + $("#saleFromDate").datepicker({ + dateFormat: "yy-mm-dd", + defaultDate: this.state.saleFromDate, + onSelect(dateText) { + self.state.saleFromDate = dateText; + }, + }).datepicker("setDate", this.state.saleFromDate); + + $("#saleToDate").datepicker({ + dateFormat: "yy-mm-dd", + defaultDate: this.state.saleToDate, + onSelect(dateText) { + self.state.saleToDate = dateText; + }, + }).datepicker("setDate", this.state.saleToDate); + } + + async loadSaleData() { + try { + const data = await this.orm.call("samashti.board", "get_sale_margin_data", [ + this.state.saleFromDate, + this.state.saleToDate + ]); + this.state.sale_rows = data || [] + this.renderSaleGrid(); + } catch (error) { + console.error("Error loading data:", error); + } + } + + async getSaleColumns() { + return [ + { title: "Sale Order", dataIndx: "sale_order", width: 100, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, + { title: "Date", dataIndx: "date", width: 150 }, + { title: "Invoice", dataIndx: "invoice", width: 180, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, + { title: "Customer", dataIndx: "customer", width: 280, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, + { title: "Tags", dataIndx: "tags", width: 100, }, + { title: "Quantity (Bags/No's)", dataIndx: "quantity", width: 150, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, + { title: "Weight (kgs)", dataIndx: "weight", width: 120 }, + { title: "Production Cost (INR)", dataIndx: "cost", width: 150, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, + { title: "Sale Price (INR)", dataIndx: "sale_price", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, + { title: "Margin (INR)", dataIndx: "margin", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, + { title: "Margin %", dataIndx: "margin_percent", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" }, + { + title: "View", + width: 120, + editable: false, + summary: false, + render: function (ui) { + return "" + }, + postRender: function (ui) { + var grid = this, + $cell = grid.getCell(ui); + $cell.find(".row-btn-view") + .button({ icons: { primary: 'ui-icon-extlink' } }) + .on("click", async function (evt) { + const res = await odoo.__WOWL_DEBUG__.root.orm.call('samashti.board', 'action_view_sale_orders', [ui.rowData.id, ui.rowData.id]) + res.target = "new" + await odoo.__WOWL_DEBUG__.root.actionService.doAction(res) + }); + } + }, + ] + } + + async renderSaleGrid() { + const columns = await this.getSaleColumns() + const agg = pq.aggregate; + + const gridOptions = { + selectionModel: { type: "row" }, + width: "100%", + height: "100%", + editable: false, + postRenderInterval: -1, + stripeRows: true, + filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 }, + dataModel: { data: this.state.sale_rows, location: "local", sorting: "local", paging: "local" }, + colModel: columns, + toolbar: { + items: [ + { + 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.export_Excel() + }, + { + 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: 'Sale Margin 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, + } + }; + $(this.gridSaleContainer.el) + .css({ height: '600px', width: '100%' }) + .pqGrid(gridOptions); + + const groupModel = { + on: true, + dataIndx: ['tags'], + checkbox: true, + checkboxHead: true, + header: true, + cascade: true, + titleInFirstCol: true, + fixCols: true, + showSummary: [true, true], + grandSummary: true, + title: [ + "{0} ({1})", + "{0} - {1}" + ] + }; + $(this.gridSaleContainer.el).pqGrid("groupOption", groupModel); + $(this.gridSaleContainer.el).pqGrid("refreshDataAndView"); + } + + async export_Excel() { + try { + if ($(this.gridSaleContainer.el).length > 0) { + const grid = $(this.gridSaleContainer.el).pqGrid('instance'); + const colModel = grid.option('colModel'); + const dataModel = grid.option('dataModel'); + const gridData = dataModel.data || dataModel; + + const response = await fetch('/sale_margin/report_excel', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + colModel: colModel, + gridData: gridData, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'Sale_Margin_Report.xlsx'; + document.body.appendChild(a); + a.click(); + a.remove(); + } + } catch (error) { + console.error('Export failed:', error); + alert('Export failed: ' + error.message); + } + } +} + +registry.category("actions").add("SaleDashboard", SaleDashboard); diff --git a/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.xml b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.xml new file mode 100644 index 000000000..cf55a2d44 --- /dev/null +++ b/custom_addons/dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.xml @@ -0,0 +1,73 @@ + + + +
+
+
+
+ +
+
+
+ +
+
+

Sale Margin Dashboard

+

+ + + + +

+
+
+
+ + +
+
+
+
+ + + + +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+
+
+
+
+ +
+
+
+ Sale Margin Data Grid + +
+
+
+
+
+
+
+
+
\ No newline at end of file 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 688a64715..4d9287e7e 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,68 +1,42 @@ /** @odoo-module **/ -//import { standardWidgetProps } from "@web/views/widgets/standard_widget_props"; +import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; import { standardActionServiceProps } from "@web/webclient/actions/action_service"; -import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl"; -import { registry } from "@web/core/registry"; -import { useService } from "@web/core/utils/hooks"; -import { loadJS, loadCSS,loadBundle } from "@web/core/assets"; - -export class SamashtiDashboard extends Component { - static props = { +export class StockDashboard extends Component { + static props = { ...standardActionServiceProps, }; - - static template = "SamashtiDashboard"; + static template = "StockDashboard"; setup() { this.orm = useService("orm"); this.gridRef = useRef("gridContainer"); - this.gridSaleContainer = useRef("gridSaleContainer"); this.notification = useService("notification"); - this.action = useService("action"); - // Reactive state this.state = useState({ - activeTab: 'stock', rows: [], - sale_rows: [], fromDate: "", - toDate: "", - saleFromDate: "", - saleToDate:"" + toDate: "" }); onWillStart(async () => { await this.loadDependencies(); this.initializeDates(); await this.loadGridData(); - await this.loadSaleData(); }); - onMounted(() => { this.initDatePickers(); if (this.gridRef.el) { this.renderGrid(); - } else { - console.error("Grid element not found"); - }; - if (this.gridSaleContainer.el) { - this.renderSaleGrid(); - } else { - console.error("Grid element not found"); } }); } - setActiveTab(tab) { - this.state.activeTab = tab; - } - // --- Load external JS/CSS dependencies async loadDependencies() { try { -// await loadJS("https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"); -// await loadCSS("https://code.jquery.com/ui/1.13.2/themes/smoothness/jquery-ui.css"); window.$ = window.jQuery = window.$ || window.jQuery; if (!window.pq) throw new Error("pqGrid failed to load"); } catch (error) { @@ -72,27 +46,21 @@ export class SamashtiDashboard extends Component { } } - // --- Initialize default From/To dates initializeDates() { const today = new Date(); const firstDay = new Date(today.getFullYear(), today.getMonth(), 1); - // Function to format a Date object to YYYY-MM-DD in local time const formatLocalDate = (date) => { const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); // months are 0-indexed + const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; this.state.fromDate = formatLocalDate(firstDay); - this.state.saleFromDate = formatLocalDate(firstDay); this.state.toDate = formatLocalDate(today); - this.state.saleToDate = formatLocalDate(today); - } - // --- Initialize jQuery UI datepickers initDatePickers() { const self = this; $("#fromDate").datepicker({ @@ -103,14 +71,6 @@ export class SamashtiDashboard extends Component { }, }).datepicker("setDate", this.state.fromDate); - $("#saleFromDate").datepicker({ - dateFormat: "yy-mm-dd", - defaultDate: this.state.saleFromDate, - onSelect(dateText) { - self.state.saleFromDate = dateText; - }, - }).datepicker("setDate", this.state.saleFromDate); - $("#toDate").datepicker({ dateFormat: "yy-mm-dd", defaultDate: this.state.toDate, @@ -118,17 +78,8 @@ export class SamashtiDashboard extends Component { self.state.toDate = dateText; }, }).datepicker("setDate", this.state.toDate); - - $("#saleToDate").datepicker({ - dateFormat: "yy-mm-dd", - defaultDate: this.state.saleToDate, - onSelect(dateText) { - self.state.saleToDate = dateText; - }, - }).datepicker("setDate", this.state.saleToDate); } - // --- Fetch grid data from backend async loadGridData() { try { const records = await this.orm.call("samashti.board", "get_stock_moves_data", [ @@ -141,182 +92,88 @@ export class SamashtiDashboard extends Component { console.error("Error loading data:", error); } } - async loadSaleData(){ - debugger; - try { - const data = await this.orm.call("samashti.board", "get_sale_margin_data",[ - this.state.saleFromDate, - this.state.saleToDate - ]); - this.state.sale_rows = data || [] - this.renderSaleGrid(); - } catch (error) { - console.error("Error loading data:", error); - } - } - - - async export_Excel() { - try { - if ($(this.gridSaleContainer.el).length > 0) { - const grid = $(this.gridSaleContainer.el).pqGrid('instance'); - const colModel = grid.option('colModel'); - const dataModel = grid.option('dataModel'); - const gridData = dataModel.data || dataModel; - - const response = await fetch('/sale_margin/report_excel', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - colModel: colModel, - gridData: gridData, - }), - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'Sale_Margin_Report.xlsx'; - document.body.appendChild(a); - a.click(); - a.remove(); - } - } catch (error) { - console.error('Export failed:', error); - alert('Export failed: ' + error.message); - } - } - - - // --- Define grid columns 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: "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" }, - + { 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" }, ]; } - async getSaleColumns(){ - return[ - { title: "Sale Order", dataIndx: "sale_order", width: 100,filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, - { title: "Date", dataIndx: "date", width: 150 }, - { title: "Invoice", dataIndx: "invoice", width: 180,filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, - { title: "Customer", dataIndx: "customer", width: 280,filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } }, - { title: "Tags", dataIndx:"tags", width:100, }, - { title: "Quantity", dataIndx: "quantity", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" }, - { title: "Weight", dataIndx: "weight", width: 150 }, - { title: "Production Cost", dataIndx: "cost", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" }, - { title: "Sale Price", dataIndx: "sale_price", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" }, - { title: "Margin", dataIndx: "margin", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" }, - { title: "Margin %", dataIndx: "margin_percent", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" }, - { - title: "View", - width: 120, - editable: false, - summary:false, - render: function (ui) { - return "" - }, - postRender: function (ui) { - var grid = this, - $cell = grid.getCell(ui); - $cell.find(".row-btn-view") - .button({ icons: { primary: 'ui-icon-extlink'} }) - .on("click", async function (evt) { - const res = await odoo.__WOWL_DEBUG__.root.orm.call('samashti.board','action_view_sale_orders',[ui.rowData.id,ui.rowData.id]) -// res.views = [[false, "form"]], - res.target = "new" - await odoo.__WOWL_DEBUG__.root.actionService.doAction(res) - }); - } - - }, - ] - } async renderGrid() { - const columns = await this.getColumns(); - const agg = pq.aggregate; // ensure pq is loaded globally + const columns = await this.getColumns(); + const agg = pq.aggregate; - const gridOptions = { - selectionModel: { type: "row" }, - width: "100%", - height: "100%", - editable: false, - stripeRows: true, - filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 }, - 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: 'jQuery grid', - format: 'htm', - render: true - }), - newWin = window.open('', '', 'width=1200, height=700'), - doc = newWin.document.open(); - doc.write(exportHtml); - doc.close(); - newWin.print(); - } + const gridOptions = { + selectionModel: { type: "row" }, + width: "100%", + height: "100%", + editable: false, + stripeRows: true, + filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 }, + 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: { + detailModel: { cache: false, } - }; + }; - // Initialize PQGrid without groupModel - var grid = $(this.gridRef.el).pqGrid() - if (grid.length){ + var grid = $(this.gridRef.el).pqGrid() + if (grid.length) { grid.pqGrid("destroy"); } - $(this.gridRef.el) - .css({ height: '600px', width: '100%' }) - .pqGrid(gridOptions); + $(this.gridRef.el) + .css({ height: '600px', width: '100%' }) + .pqGrid(gridOptions); - // Then set groupModel using groupOption - const groupModel = { + const groupModel = { on: true, checkbox: true, checkboxHead: true, - header:true, + header: true, cascade: true, titleInFirstCol: true, fixCols: true, @@ -327,89 +184,15 @@ export class SamashtiDashboard extends Component { "{0} - {1}" ] }; - $(this.gridRef.el).pqGrid("groupOption", groupModel); - - // Refresh grid to apply grouping - $(this.gridRef.el).pqGrid("refreshDataAndView"); -} - async renderSaleGrid(){ - const columns = await this.getSaleColumns() - const agg = pq.aggregate; - - const gridOptions = { - selectionModel: { type: "row" }, - width: "100%", - height: "100%", - editable: false, - postRenderInterval: -1, - stripeRows: true, - filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 }, - dataModel: { data: this.state.sale_rows, location: "local", sorting: "local", paging: "local" }, - colModel: columns, - toolbar: { - items: [ - { 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.export_Excel() }, - - { - 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: 'jQuery grid', - 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, - } - }; - $(this.gridSaleContainer.el) - .css({ height: '600px', width: '100%' }) - .pqGrid(gridOptions); - // Then set groupModel using groupOption - const groupModel = { - on: true, - dataIndx: ['tags'], - checkbox: true, - checkboxHead: true, - header:true, - cascade: true, - titleInFirstCol: true, - fixCols: true, - showSummary: [true, true], - grandSummary: true, - title: [ - "{0} ({1})", - "{0} - {1}" - ] - }; - $(this.gridSaleContainer.el).pqGrid("groupOption", groupModel); - - // Refresh grid to apply grouping - $(this.gridSaleContainer.el).pqGrid("refreshDataAndView"); - } + $(this.gridRef.el).pqGrid("groupOption", groupModel); + $(this.gridRef.el).pqGrid("refreshDataAndView"); + } exportGrid() { const format_ex = $("#export_format").val(); const grid = $(this.gridRef.el); - console.log("Starting export with format:", format_ex); - - // Different handling for different formats - switch(format_ex) { + switch (format_ex) { case 'xlsx': const blobXlsx = grid.pqGrid("exportData", { format: 'xlsx', @@ -423,7 +206,6 @@ export class SamashtiDashboard extends Component { format: 'csv', render: true }); - // CSV often returns as string if (typeof blobCsv === 'string') { saveAs(new Blob([blobCsv], { type: 'text/csv' }), "StockData.csv"); } else { @@ -462,6 +244,4 @@ export class SamashtiDashboard extends Component { } } - -registry.category("actions").add("SamashtiDashboard", SamashtiDashboard); - +registry.category("actions").add("StockDashboard", StockDashboard); 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 0d2991fd4..2336e1634 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,134 +1,73 @@ - +
- - -
-
-
- - +
+
+
+ +
+
+
+ +
+
+

Stock Dashboard

+

+ + + + +

- -
- -
-
-
- -

- Samashti Stock Dashboard -

- -

- to -

-
- -
- - -
- - -
- - -
- - -
- -
-
+ +
+
+
+
+ + + +
-
- -
-
-
-

- Samashti Sale Margin Dashboard -

- -

- to -

-
- -
- - -
- - -
- - -
- - -
- -
-
+
+
+ + + +
+ +
+ +
+
+
+
- - - - -
-
-
- Stock Data Grid - -
-
-
-
-
+
+
+
+ Stock Data Grid + +
- - - - -
-
-
- Sale Margin Data Grid - -
-
-
-
-
+
+
- - +
\ No newline at end of file diff --git a/custom_addons/dashboard/views/pqgrid_dashboard.xml b/custom_addons/dashboard/views/pqgrid_dashboard.xml index db1f77951..4a1c22f9b 100644 --- a/custom_addons/dashboard/views/pqgrid_dashboard.xml +++ b/custom_addons/dashboard/views/pqgrid_dashboard.xml @@ -1,14 +1,45 @@ - - - Dashboard - SamashtiDashboard - {'user_id':uid} - - + + + Stock Dashboard + StockDashboard + {'user_id':uid} + + + + + + Sale Margin Dashboard + SaleDashboard + {'user_id':uid} + + + + + + Consumption Dashboard + ConsumptionDashboard + + + \ No newline at end of file