dash board changes
This commit is contained in:
parent
4b90321123
commit
aa1fd0c881
|
|
@ -16,7 +16,11 @@
|
||||||
('include', 'web_grid._assets_pqgrid'),
|
('include', 'web_grid._assets_pqgrid'),
|
||||||
# Internal module JS and XML files (ensure correct paths within 'static/src')
|
# 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_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_stock_dashboard.xml',
|
||||||
|
'dashboard/static/src/components/pqgrid_dashboard/pqgrid_sale_dashboard.xml',
|
||||||
|
'dashboard/static/src/components/pqgrid_dashboard/pqgrid_consumption_dashboard.xml',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class SaleMarginController(http.Controller):
|
||||||
'bg_color': '#366092', 'border': 1
|
'bg_color': '#366092', 'border': 1
|
||||||
})
|
})
|
||||||
text_fmt = workbook.add_format({
|
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({
|
float_fmt = workbook.add_format({
|
||||||
'font_size': 10, 'align': 'right', 'valign': 'vcenter',
|
'font_size': 10, 'align': 'right', 'valign': 'vcenter',
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ class SamashtiDashboard(models.AbstractModel):
|
||||||
# Combine invoice names and dates
|
# Combine invoice names and dates
|
||||||
if order.invoice_ids:
|
if order.invoice_ids:
|
||||||
invoice = ', '.join(inv.name for inv in 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:
|
else:
|
||||||
invoice = "N/A"
|
invoice = "N/A"
|
||||||
date = "No invoices"
|
date = "No invoices"
|
||||||
|
|
@ -228,5 +228,50 @@ class SamashtiDashboard(models.AbstractModel):
|
||||||
result['target'] = 'self',
|
result['target'] = 'self',
|
||||||
return result
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,68 +1,42 @@
|
||||||
/** @odoo-module **/
|
/** @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 { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||||
|
|
||||||
import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl";
|
export class StockDashboard extends Component {
|
||||||
import { registry } from "@web/core/registry";
|
static props = {
|
||||||
import { useService } from "@web/core/utils/hooks";
|
|
||||||
import { loadJS, loadCSS,loadBundle } from "@web/core/assets";
|
|
||||||
|
|
||||||
export class SamashtiDashboard extends Component {
|
|
||||||
static props = {
|
|
||||||
...standardActionServiceProps,
|
...standardActionServiceProps,
|
||||||
};
|
};
|
||||||
|
static template = "StockDashboard";
|
||||||
static template = "SamashtiDashboard";
|
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.orm = useService("orm");
|
this.orm = useService("orm");
|
||||||
this.gridRef = useRef("gridContainer");
|
this.gridRef = useRef("gridContainer");
|
||||||
this.gridSaleContainer = useRef("gridSaleContainer");
|
|
||||||
this.notification = useService("notification");
|
this.notification = useService("notification");
|
||||||
this.action = useService("action");
|
|
||||||
|
|
||||||
// Reactive state
|
|
||||||
this.state = useState({
|
this.state = useState({
|
||||||
activeTab: 'stock',
|
|
||||||
rows: [],
|
rows: [],
|
||||||
sale_rows: [],
|
|
||||||
fromDate: "",
|
fromDate: "",
|
||||||
toDate: "",
|
toDate: ""
|
||||||
saleFromDate: "",
|
|
||||||
saleToDate:""
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onWillStart(async () => {
|
onWillStart(async () => {
|
||||||
await this.loadDependencies();
|
await this.loadDependencies();
|
||||||
this.initializeDates();
|
this.initializeDates();
|
||||||
await this.loadGridData();
|
await this.loadGridData();
|
||||||
await this.loadSaleData();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
this.initDatePickers();
|
this.initDatePickers();
|
||||||
if (this.gridRef.el) {
|
if (this.gridRef.el) {
|
||||||
this.renderGrid();
|
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() {
|
async loadDependencies() {
|
||||||
try {
|
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;
|
window.$ = window.jQuery = window.$ || window.jQuery;
|
||||||
if (!window.pq) throw new Error("pqGrid failed to load");
|
if (!window.pq) throw new Error("pqGrid failed to load");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -72,27 +46,21 @@ export class SamashtiDashboard extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Initialize default From/To dates
|
|
||||||
initializeDates() {
|
initializeDates() {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
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 formatLocalDate = (date) => {
|
||||||
const year = date.getFullYear();
|
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');
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state.fromDate = formatLocalDate(firstDay);
|
this.state.fromDate = formatLocalDate(firstDay);
|
||||||
this.state.saleFromDate = formatLocalDate(firstDay);
|
|
||||||
this.state.toDate = formatLocalDate(today);
|
this.state.toDate = formatLocalDate(today);
|
||||||
this.state.saleToDate = formatLocalDate(today);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Initialize jQuery UI datepickers
|
|
||||||
initDatePickers() {
|
initDatePickers() {
|
||||||
const self = this;
|
const self = this;
|
||||||
$("#fromDate").datepicker({
|
$("#fromDate").datepicker({
|
||||||
|
|
@ -103,14 +71,6 @@ export class SamashtiDashboard extends Component {
|
||||||
},
|
},
|
||||||
}).datepicker("setDate", this.state.fromDate);
|
}).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({
|
$("#toDate").datepicker({
|
||||||
dateFormat: "yy-mm-dd",
|
dateFormat: "yy-mm-dd",
|
||||||
defaultDate: this.state.toDate,
|
defaultDate: this.state.toDate,
|
||||||
|
|
@ -118,17 +78,8 @@ export class SamashtiDashboard extends Component {
|
||||||
self.state.toDate = dateText;
|
self.state.toDate = dateText;
|
||||||
},
|
},
|
||||||
}).datepicker("setDate", this.state.toDate);
|
}).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() {
|
async loadGridData() {
|
||||||
try {
|
try {
|
||||||
const records = await this.orm.call("samashti.board", "get_stock_moves_data", [
|
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);
|
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() {
|
async getColumns() {
|
||||||
return [
|
return [
|
||||||
{ title: "Product Code", dataIndx: "product_code", width: 100,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: "Product Name", dataIndx: "product_name", width: 280, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||||
{ title: "Category", dataIndx: "category", width: 150 },
|
{ title: "Category", dataIndx: "category", width: 150 },
|
||||||
{ title: "Opening Stock", dataIndx: "opening_stock", 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: "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: "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: "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: "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: "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: "Uom", dataIndx: "uom", width: 90, dataType: "text" },
|
||||||
{ title: "Value", dataIndx: "value", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
{ 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() {
|
async renderGrid() {
|
||||||
const columns = await this.getColumns();
|
const columns = await this.getColumns();
|
||||||
const agg = pq.aggregate; // ensure pq is loaded globally
|
const agg = pq.aggregate;
|
||||||
|
|
||||||
const gridOptions = {
|
const gridOptions = {
|
||||||
selectionModel: { type: "row" },
|
selectionModel: { type: "row" },
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
editable: false,
|
editable: false,
|
||||||
stripeRows: true,
|
stripeRows: true,
|
||||||
filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 },
|
filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 },
|
||||||
dataModel: { data: this.state.rows, location: "local", sorting: "local", paging: "local" },
|
dataModel: { data: this.state.rows, location: "local", sorting: "local", paging: "local" },
|
||||||
colModel: columns,
|
colModel: columns,
|
||||||
toolbar: {
|
toolbar: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: '<span style="color: #555; font-size: 13px;">Format:</span>',
|
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;"',
|
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",
|
{
|
||||||
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);"',
|
type: "button", label: "Export", icon: "ui-icon-arrowthickstop-1-s",
|
||||||
listener: () => this.exportGrid() },
|
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',
|
type: 'button',
|
||||||
label: 'Print',
|
icon: 'ui-icon-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);"',
|
label: 'Print',
|
||||||
listener: function() {
|
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);"',
|
||||||
var exportHtml = this.exportData({
|
listener: function () {
|
||||||
title: 'jQuery grid',
|
var exportHtml = this.exportData({
|
||||||
format: 'htm',
|
title: 'Stock Report',
|
||||||
render: true
|
format: 'htm',
|
||||||
}),
|
render: true
|
||||||
newWin = window.open('', '', 'width=1200, height=700'),
|
}),
|
||||||
doc = newWin.document.open();
|
newWin = window.open('', '', 'width=1200, height=700'),
|
||||||
doc.write(exportHtml);
|
doc = newWin.document.open();
|
||||||
doc.close();
|
doc.write(exportHtml);
|
||||||
newWin.print();
|
doc.close();
|
||||||
}
|
newWin.print();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
detailModel: {
|
||||||
},
|
|
||||||
detailModel: {
|
|
||||||
cache: false,
|
cache: false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize PQGrid without groupModel
|
var grid = $(this.gridRef.el).pqGrid()
|
||||||
var grid = $(this.gridRef.el).pqGrid()
|
if (grid.length) {
|
||||||
if (grid.length){
|
|
||||||
grid.pqGrid("destroy");
|
grid.pqGrid("destroy");
|
||||||
}
|
}
|
||||||
|
|
||||||
$(this.gridRef.el)
|
$(this.gridRef.el)
|
||||||
.css({ height: '600px', width: '100%' })
|
.css({ height: '600px', width: '100%' })
|
||||||
.pqGrid(gridOptions);
|
.pqGrid(gridOptions);
|
||||||
|
|
||||||
// Then set groupModel using groupOption
|
const groupModel = {
|
||||||
const groupModel = {
|
|
||||||
on: true,
|
on: true,
|
||||||
checkbox: true,
|
checkbox: true,
|
||||||
checkboxHead: true,
|
checkboxHead: true,
|
||||||
header:true,
|
header: true,
|
||||||
cascade: true,
|
cascade: true,
|
||||||
titleInFirstCol: true,
|
titleInFirstCol: true,
|
||||||
fixCols: true,
|
fixCols: true,
|
||||||
|
|
@ -327,89 +184,15 @@ export class SamashtiDashboard extends Component {
|
||||||
"{0} - {1}"
|
"{0} - {1}"
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
$(this.gridRef.el).pqGrid("groupOption", groupModel);
|
$(this.gridRef.el).pqGrid("groupOption", groupModel);
|
||||||
|
$(this.gridRef.el).pqGrid("refreshDataAndView");
|
||||||
// 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() {
|
exportGrid() {
|
||||||
const format_ex = $("#export_format").val();
|
const format_ex = $("#export_format").val();
|
||||||
const grid = $(this.gridRef.el);
|
const grid = $(this.gridRef.el);
|
||||||
|
|
||||||
console.log("Starting export with format:", format_ex);
|
switch (format_ex) {
|
||||||
|
|
||||||
// Different handling for different formats
|
|
||||||
switch(format_ex) {
|
|
||||||
case 'xlsx':
|
case 'xlsx':
|
||||||
const blobXlsx = grid.pqGrid("exportData", {
|
const blobXlsx = grid.pqGrid("exportData", {
|
||||||
format: 'xlsx',
|
format: 'xlsx',
|
||||||
|
|
@ -423,7 +206,6 @@ export class SamashtiDashboard extends Component {
|
||||||
format: 'csv',
|
format: 'csv',
|
||||||
render: true
|
render: true
|
||||||
});
|
});
|
||||||
// CSV often returns as string
|
|
||||||
if (typeof blobCsv === 'string') {
|
if (typeof blobCsv === 'string') {
|
||||||
saveAs(new Blob([blobCsv], { type: 'text/csv' }), "StockData.csv");
|
saveAs(new Blob([blobCsv], { type: 'text/csv' }), "StockData.csv");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -462,6 +244,4 @@ export class SamashtiDashboard extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registry.category("actions").add("StockDashboard", StockDashboard);
|
||||||
registry.category("actions").add("SamashtiDashboard", SamashtiDashboard);
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,134 +1,73 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<templates xml:space="preserve">
|
<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;">
|
<div class="p-4" style="height: 100%; overflow-y: auto;">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
<!-- 🔹 Dashboard Selection Tabs -->
|
<div class="card-body">
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="row align-items-center">
|
||||||
<div class="card-body text-center">
|
<!-- Left Side: Title with Icon -->
|
||||||
<div class="btn-group" role="group">
|
<div class="col-md-6">
|
||||||
<button type="button"
|
<div class="d-flex align-items-center">
|
||||||
class="btn"
|
<div class="bg-primary rounded-circle p-2 me-3">
|
||||||
t-att-class="state.activeTab === 'stock' ? 'btn-primary' : 'btn-outline-primary'"
|
<i class="fa fa-cubes text-white"></i>
|
||||||
t-on-click="() => this.setActiveTab('stock')">
|
</div>
|
||||||
Stock Dashboard
|
<div>
|
||||||
</button>
|
<h4 class="text-primary fw-bold mb-1">Stock Dashboard</h4>
|
||||||
<button type="button"
|
<p class="text-muted mb-0 small">
|
||||||
class="btn"
|
<i class="fa fa-calendar me-1"></i>
|
||||||
t-att-class="state.activeTab === 'sale' ? 'btn-success' : 'btn-outline-success'"
|
<span t-esc="state.fromDate || 'Start Date'"/>
|
||||||
t-on-click="() => this.setActiveTab('sale')">
|
<i class="fa fa-arrow-right mx-2"></i>
|
||||||
Sale Margin Dashboard
|
<span t-esc="state.toDate || 'End Date'"/>
|
||||||
</button>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🔹 Cards Side by Side -->
|
<!-- Right Side: Date Controls -->
|
||||||
<div class="row mb-4">
|
<div class="col-md-6">
|
||||||
<!-- Stock Dashboard Card -->
|
<div class="row g-2 align-items-center justify-content-end">
|
||||||
<div class="col-md-6">
|
<div class="col-auto">
|
||||||
<div class="card shadow-sm">
|
<div class="input-group input-group-sm">
|
||||||
<div class="card-body">
|
<span class="input-group-text">
|
||||||
|
<i class="fa fa-calendar"></i>
|
||||||
<h4 class="text-center text-primary fw-bold mb-3">
|
</span>
|
||||||
Samashti Stock Dashboard
|
<input type="text" id="fromDate" t-model="state.fromDate"
|
||||||
</h4>
|
class="form-control form-control-sm" placeholder="From Date"/>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sale Dashboard Card -->
|
<div class="col-auto">
|
||||||
<div class="col-md-6">
|
<div class="input-group input-group-sm">
|
||||||
<div class="card shadow-sm h-100">
|
<span class="input-group-text">
|
||||||
<div class="card-body">
|
<i class="fa fa-calendar"></i>
|
||||||
<h4 class="text-center text-success fw-bold mb-3">
|
</span>
|
||||||
Samashti Sale Margin Dashboard
|
<input type="text" id="toDate" t-model="state.toDate"
|
||||||
</h4>
|
class="form-control form-control-sm" placeholder="To Date"/>
|
||||||
|
|
||||||
<p class="text-center text-muted mb-3">
|
|
||||||
<span t-esc="state.saleFromDate || 'Start Date'"/> to <span t-esc="state.saleToDate || '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>
|
|
||||||
|
|
||||||
<!-- 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 "/>
|
|
||||||
</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
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 🔹 Grid Sections (Conditional Display) -->
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
<!-- Stock Grid -->
|
<h5 class="mb-0">
|
||||||
<t t-if="state.activeTab === 'stock'">
|
<i class="fa fa-cubes me-2"></i>Stock Data Grid
|
||||||
<div class="card shadow-sm">
|
<small class="float-end" t-esc="'Showing data from ' + (state.fromDate || 'start') + ' to ' + (state.toDate || 'end')"/>
|
||||||
<div class="card-header bg-primary text-white">
|
</h5>
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="fa fa-cubes me-2"></i>Stock Data Grid
|
|
||||||
<small class="float-end" t-esc="'Showing data from ' + (state.fromDate || 'start') + ' to ' + (state.toDate || 'end')"/>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" style="padding: 0;">
|
|
||||||
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</t>
|
<div class="card-body" style="padding: 0;">
|
||||||
|
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
|
||||||
<!-- 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>
|
</div>
|
||||||
</t>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|
@ -1,14 +1,45 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<odoo>
|
<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"
|
<menuitem id="samashti_dashboard_root"
|
||||||
name="Overview"
|
name="Overview"
|
||||||
action="samashti_action_dashboards"
|
action="stock_dashboard_action"
|
||||||
sequence="-100" groups="dashboard.group_proforma_sales_dashboard"/>
|
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>
|
</odoo>
|
||||||
Loading…
Reference in New Issue