dash board changes

This commit is contained in:
raman 2025-11-14 17:22:30 +05:30
parent 4b90321123
commit aa1fd0c881
10 changed files with 1070 additions and 419 deletions

View File

@ -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',
],

View File

@ -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',

View File

@ -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

View File

@ -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 `<span >${value}</span>`;
// }
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: '<span style="color: #555; font-size: 13px;">Format:</span>',
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);

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="ConsumptionDashboard" owl="1">
<div class="p-4" style="height: 100%; overflow-y: auto;">
<!-- Header Card -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row align-items-center">
<!-- Left Side: Title with Icon -->
<div class="col-md-6">
<div class="d-flex align-items-center">
<div class="bg-warning rounded-circle p-2 me-3">
<i class="fa fa-bar-chart text-white"></i>
</div>
<div>
<h4 class="text-warning fw-bold mb-1">Consolidated Consumption Report</h4>
<p class="text-muted mb-0 small">
<i class="fa fa-calendar me-1"></i>
<span t-esc="state.fromDate || 'Start Date'"/>
<i class="fa fa-arrow-right mx-2"></i>
<span t-esc="state.toDate || 'End Date'"/>
</p>
</div>
</div>
</div>
<!-- Right Side: Date Controls -->
<div class="col-md-6">
<div class="row g-2 align-items-center justify-content-end">
<div class="col-auto">
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="fa fa-calendar"></i>
</span>
<input type="text" id="fromDate" t-model="state.fromDate"
class="form-control form-control-sm" placeholder="From Date"/>
</div>
</div>
<div class="col-auto">
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="fa fa-calendar"></i>
</span>
<input type="text" id="toDate" t-model="state.toDate"
class="form-control form-control-sm" placeholder="To Date"/>
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-warning btn-sm" t-on-click="loadGridData">
<i class="fa fa-refresh me-1"></i> Update
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<!-- <div class="col-md-3">-->
<!-- <div class="card border-0 bg-light-info">-->
<!-- <div class="card-body text-center">-->
<!-- <h6 class="card-title text-info">Total Products</h6>-->
<!-- <h3 class="text-info" t-esc="state.rows.length"/>-->
<!-- <small class="text-muted">Items in report</small>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="col-md-3">-->
<!-- <div class="card border-0 bg-light-success">-->
<!-- <div class="card-body text-center">-->
<!-- <h6 class="card-title text-success">Total Consumption</h6>-->
<!-- <h3 class="text-success" t-esc="this.getTotalConsumption()"/>-->
<!-- <small class="text-muted">Units consumed</small>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="col-md-3">-->
<!-- <div class="card border-0 bg-light-danger">-->
<!-- <div class="card-body text-center">-->
<!-- <h6 class="card-title text-danger">Low Stock Items</h6>-->
<!-- <h3 class="text-danger" t-esc="this.getLowStockCount()"/>-->
<!-- <small class="text-muted">Below reorder point</small>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="col-md-3">-->
<!-- <div class="card border-0 bg-light-warning">-->
<!-- <div class="card-body text-center">-->
<!-- <h6 class="card-title text-warning">Avg Consumption</h6>-->
<!-- <h3 class="text-warning" t-esc="this.getAvgConsumption()"/>-->
<!-- <small class="text-muted">Units per day</small>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
<!-- Main Grid -->
<div class="card shadow-sm">
<div class="card-header bg-warning text-white">
<h5 class="mb-0">
<i class="fa fa-table me-2"></i>Consumption Analysis
<small class="float-end" t-esc="'Showing ' + state.rows.length + ' products'"/>
</h5>
</div>
<div class="card-body" style="padding: 0;">
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -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 "<button class='row-btn-view' type='button' >View</button>"
},
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);

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="SaleDashboard" owl="1">
<div class="p-4" style="height: 100%; overflow-y: auto;">
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row align-items-center">
<!-- Left Side: Title with Icon -->
<div class="col-md-6">
<div class="d-flex align-items-center">
<div class="bg-success rounded-circle p-2 me-3">
<i class="fa fa-chart-line text-white"></i>
</div>
<div>
<h4 class="text-success fw-bold mb-1">Sale Margin Dashboard</h4>
<p class="text-muted mb-0 small">
<i class="fa fa-calendar me-1"></i>
<span t-esc="state.saleFromDate || 'Start Date'"/>
<i class="fa fa-arrow-right mx-2"></i>
<span t-esc="state.saleToDate || 'End Date'"/>
</p>
</div>
</div>
</div>
<!-- Right Side: Date Controls -->
<div class="col-md-6">
<div class="row g-2 align-items-center justify-content-end">
<div class="col-auto">
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="fa fa-calendar"></i>
</span>
<input type="text" id="saleFromDate" t-model="state.saleFromDate"
class="form-control form-control-sm" placeholder="From Date"/>
</div>
</div>
<div class="col-auto">
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="fa fa-calendar"></i>
</span>
<input type="text" id="saleToDate" t-model="state.saleToDate"
class="form-control form-control-sm" placeholder="To Date"/>
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-success btn-sm" t-on-click="loadSaleData">
<i class="fa fa-refresh me-1"></i> Update
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="fa fa-chart-line me-2"></i>Sale Margin Data Grid
<small class="float-end" t-esc="'Showing data from ' + (state.saleFromDate || 'start') + ' to ' + (state.saleToDate || 'end')"/>
</h5>
</div>
<div class="card-body" style="padding: 0;">
<div t-ref="gridSaleContainer" style="width: 100%; height: 600px;"></div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -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 {
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,119 +92,26 @@ 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 "<button class='row-btn-view' type='button' >View</button>"
},
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 agg = pq.aggregate;
const gridOptions = {
selectionModel: { type: "row" },
@ -270,20 +128,21 @@ export class SamashtiDashboard extends Component {
type: 'select',
label: '<span style="color: #555; font-size: 13px;">Format:</span>',
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'}]
options: [{ xlsx: '📊 Excel', csv: '📝 CSV', htm: '🌐 HTML', json: '🔤 JSON' }]
},
{ type: "button", label: "Export", icon: "ui-icon-arrowthickstop-1-s",
{
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() },
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() {
listener: function () {
var exportHtml = this.exportData({
title: 'jQuery grid',
title: 'Stock Report',
format: 'htm',
render: true
}),
@ -301,9 +160,8 @@ export class SamashtiDashboard extends Component {
}
};
// Initialize PQGrid without groupModel
var grid = $(this.gridRef.el).pqGrid()
if (grid.length){
if (grid.length) {
grid.pqGrid("destroy");
}
@ -311,12 +169,11 @@ export class SamashtiDashboard extends Component {
.css({ height: '600px', width: '100%' })
.pqGrid(gridOptions);
// Then set groupModel using groupOption
const groupModel = {
on: true,
checkbox: true,
checkboxHead: true,
header:true,
header: true,
cascade: true,
titleInFirstCol: true,
fixCols: true,
@ -328,88 +185,14 @@ export class SamashtiDashboard extends Component {
]
};
$(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");
}
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);

View File

@ -1,106 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="SamashtiDashboard" owl="1">
<t t-name="StockDashboard" owl="1">
<div class="p-4" style="height: 100%; overflow-y: auto;">
<!-- 🔹 Dashboard Selection Tabs -->
<div class="card shadow-sm mb-4">
<div class="card-body text-center">
<div class="btn-group" role="group">
<button type="button"
class="btn"
t-att-class="state.activeTab === 'stock' ? 'btn-primary' : 'btn-outline-primary'"
t-on-click="() => this.setActiveTab('stock')">
Stock Dashboard
</button>
<button type="button"
class="btn"
t-att-class="state.activeTab === 'sale' ? 'btn-success' : 'btn-outline-success'"
t-on-click="() => this.setActiveTab('sale')">
Sale Margin Dashboard
</button>
</div>
</div>
</div>
<!-- 🔹 Cards Side by Side -->
<div class="row mb-4">
<!-- Stock Dashboard Card -->
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-body">
<h4 class="text-center text-primary fw-bold mb-3">
Samashti Stock Dashboard
</h4>
<p class="text-center text-muted mb-3">
<span t-esc="state.fromDate || 'Start Date'"/> to <span t-esc="state.toDate || 'End Date'"/>
</p>
<div class="row g-3 align-items-end">
<!-- From Date -->
<div class="col-lg-5 col-md-6 col-12">
<label class="form-label fw-semibold">From Date</label>
<input type="text" id="fromDate" t-model="state.fromDate" class="form-control"/>
</div>
<!-- To Date -->
<div class="col-lg-5 col-md-6 col-12">
<label class="form-label fw-semibold">To Date</label>
<input type="text" id="toDate" t-model="state.toDate" class="form-control"/>
</div>
<!-- Load Button -->
<div class="col-lg-2 col-md-12 col-12">
<button type="button" class="btn btn-primary w-100" t-on-click="loadGridData">
<i class="fa fa-sync me-1"></i> Load
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Sale Dashboard Card -->
<div class="row align-items-center">
<!-- Left Side: Title with Icon -->
<div class="col-md-6">
<div class="card shadow-sm h-100">
<div class="card-body">
<h4 class="text-center text-success fw-bold mb-3">
Samashti Sale Margin Dashboard
</h4>
<p class="text-center text-muted mb-3">
<span t-esc="state.saleFromDate || 'Start Date'"/> to <span t-esc="state.saleToDate || 'End Date'"/>
<div class="d-flex align-items-center">
<div class="bg-primary rounded-circle p-2 me-3">
<i class="fa fa-cubes text-white"></i>
</div>
<div>
<h4 class="text-primary fw-bold mb-1">Stock Dashboard</h4>
<p class="text-muted mb-0 small">
<i class="fa fa-calendar me-1"></i>
<span t-esc="state.fromDate || 'Start Date'"/>
<i class="fa fa-arrow-right mx-2"></i>
<span t-esc="state.toDate || 'End Date'"/>
</p>
<div class="row g-3 align-items-end">
<!-- From Date -->
<div class="col-lg-5 col-md-6 col-12">
<label class="form-label fw-semibold">From Date</label>
<input type="text" id="saleFromDate" t-model="state.saleFromDate" class="form-control"/>
</div>
</div>
</div>
<!-- To Date -->
<div class="col-lg-5 col-md-6 col-12">
<label class="form-label fw-semibold">To Date</label>
<input type="text" id="saleToDate" t-model="state.saleToDate" class="form-control "/>
<!-- Right Side: Date Controls -->
<div class="col-md-6">
<div class="row g-2 align-items-center justify-content-end">
<div class="col-auto">
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="fa fa-calendar"></i>
</span>
<input type="text" id="fromDate" t-model="state.fromDate"
class="form-control form-control-sm" placeholder="From Date"/>
</div>
</div>
<!-- Load Button -->
<div class="col-lg-2 col-md-12 col-12">
<button type="button" class="btn btn-success w-100" t-on-click="loadSaleData">
<i class="fa fa-sync me-1"></i> Load
<div class="col-auto">
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="fa fa-calendar"></i>
</span>
<input type="text" id="toDate" t-model="state.toDate"
class="form-control form-control-sm" placeholder="To Date"/>
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary btn-sm" t-on-click="loadGridData">
<i class="fa fa-refresh me-1"></i> Update
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 🔹 Grid Sections (Conditional Display) -->
<!-- Stock Grid -->
<t t-if="state.activeTab === 'stock'">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
@ -112,23 +68,6 @@
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
</div>
</div>
</t>
<!-- Sale Grid -->
<t t-if="state.activeTab === 'sale'">
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="fa fa-chart-line me-2"></i>Sale Margin Data Grid
<small class="float-end" t-esc="'Showing data from ' + (state.saleFromDate || 'start') + ' to ' + (state.saleToDate || 'end')"/>
</h5>
</div>
<div class="card-body" style="padding: 0;">
<div t-ref="gridSaleContainer" style="width: 100%; height: 600px;"></div>
</div>
</div>
</t>
</div>
</t>
</templates>

View File

@ -1,14 +1,45 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Client action record for Dashboard -->
<record id="samashti_action_dashboards" model="ir.actions.client">
<field name="name">Dashboard</field>
<field name="tag">SamashtiDashboard</field>
<field name="context">{'user_id':uid}</field>
</record>
<!-- Menu record for dashboard -->
<menuitem id="samashti_dashboard_root"
name="Overview"
action="samashti_action_dashboards"
action="stock_dashboard_action"
sequence="-100" groups="dashboard.group_proforma_sales_dashboard"/>
<!-- Stock Dashboard -->
<record id="stock_dashboard_action" model="ir.actions.client">
<field name="name">Stock Dashboard</field>
<field name="tag">StockDashboard</field>
<field name="context">{'user_id':uid}</field>
</record>
<menuitem id="stock_dashboard_menu"
name="Stock Dashboard"
action="stock_dashboard_action"
sequence="10"
parent="samashti_dashboard_root"
groups="dashboard.group_proforma_sales_dashboard"/>
<record id="sale_dashboard_action" model="ir.actions.client">
<field name="name">Sale Margin Dashboard</field>
<field name="tag">SaleDashboard</field>
<field name="context">{'user_id':uid}</field>
</record>
<menuitem id="sale_dashboard_menu"
name="Sale Margin Report"
action="sale_dashboard_action"
sequence="20"
parent="samashti_dashboard_root"
groups="dashboard.group_proforma_sales_dashboard"/>
<record id="consumption_dashboard_action" model="ir.actions.client">
<field name="name">Consumption Dashboard</field>
<field name="tag">ConsumptionDashboard</field>
</record>
<menuitem id="consumption_dashboard_menu"
name="Consumption Report"
action="consumption_dashboard_action"
sequence="30"
parent="samashti_dashboard_root"
groups="base.group_user"/>
</odoo>