dash board changes
This commit is contained in:
parent
4b90321123
commit
aa1fd0c881
|
|
@ -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',
|
||||
],
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 **/
|
||||
//import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
|
||||
import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { loadJS, loadCSS,loadBundle } from "@web/core/assets";
|
||||
|
||||
export class SamashtiDashboard extends Component {
|
||||
static props = {
|
||||
export class StockDashboard extends Component {
|
||||
static props = {
|
||||
...standardActionServiceProps,
|
||||
};
|
||||
|
||||
static template = "SamashtiDashboard";
|
||||
static template = "StockDashboard";
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.gridRef = useRef("gridContainer");
|
||||
this.gridSaleContainer = useRef("gridSaleContainer");
|
||||
this.notification = useService("notification");
|
||||
this.action = useService("action");
|
||||
|
||||
// Reactive state
|
||||
this.state = useState({
|
||||
activeTab: 'stock',
|
||||
rows: [],
|
||||
sale_rows: [],
|
||||
fromDate: "",
|
||||
toDate: "",
|
||||
saleFromDate: "",
|
||||
saleToDate:""
|
||||
toDate: ""
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.loadDependencies();
|
||||
this.initializeDates();
|
||||
await this.loadGridData();
|
||||
await this.loadSaleData();
|
||||
});
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
this.initDatePickers();
|
||||
if (this.gridRef.el) {
|
||||
this.renderGrid();
|
||||
} else {
|
||||
console.error("Grid element not found");
|
||||
};
|
||||
if (this.gridSaleContainer.el) {
|
||||
this.renderSaleGrid();
|
||||
} else {
|
||||
console.error("Grid element not found");
|
||||
}
|
||||
});
|
||||
}
|
||||
setActiveTab(tab) {
|
||||
this.state.activeTab = tab;
|
||||
}
|
||||
|
||||
// --- Load external JS/CSS dependencies
|
||||
async loadDependencies() {
|
||||
try {
|
||||
// await loadJS("https://code.jquery.com/ui/1.13.2/jquery-ui.min.js");
|
||||
// await loadCSS("https://code.jquery.com/ui/1.13.2/themes/smoothness/jquery-ui.css");
|
||||
window.$ = window.jQuery = window.$ || window.jQuery;
|
||||
if (!window.pq) throw new Error("pqGrid failed to load");
|
||||
} catch (error) {
|
||||
|
|
@ -72,27 +46,21 @@ export class SamashtiDashboard extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Initialize default From/To dates
|
||||
initializeDates() {
|
||||
const today = new Date();
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
|
||||
// Function to format a Date object to YYYY-MM-DD in local time
|
||||
const formatLocalDate = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // months are 0-indexed
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
this.state.fromDate = formatLocalDate(firstDay);
|
||||
this.state.saleFromDate = formatLocalDate(firstDay);
|
||||
this.state.toDate = formatLocalDate(today);
|
||||
this.state.saleToDate = formatLocalDate(today);
|
||||
|
||||
}
|
||||
|
||||
// --- Initialize jQuery UI datepickers
|
||||
initDatePickers() {
|
||||
const self = this;
|
||||
$("#fromDate").datepicker({
|
||||
|
|
@ -103,14 +71,6 @@ export class SamashtiDashboard extends Component {
|
|||
},
|
||||
}).datepicker("setDate", this.state.fromDate);
|
||||
|
||||
$("#saleFromDate").datepicker({
|
||||
dateFormat: "yy-mm-dd",
|
||||
defaultDate: this.state.saleFromDate,
|
||||
onSelect(dateText) {
|
||||
self.state.saleFromDate = dateText;
|
||||
},
|
||||
}).datepicker("setDate", this.state.saleFromDate);
|
||||
|
||||
$("#toDate").datepicker({
|
||||
dateFormat: "yy-mm-dd",
|
||||
defaultDate: this.state.toDate,
|
||||
|
|
@ -118,17 +78,8 @@ export class SamashtiDashboard extends Component {
|
|||
self.state.toDate = dateText;
|
||||
},
|
||||
}).datepicker("setDate", this.state.toDate);
|
||||
|
||||
$("#saleToDate").datepicker({
|
||||
dateFormat: "yy-mm-dd",
|
||||
defaultDate: this.state.saleToDate,
|
||||
onSelect(dateText) {
|
||||
self.state.saleToDate = dateText;
|
||||
},
|
||||
}).datepicker("setDate", this.state.saleToDate);
|
||||
}
|
||||
|
||||
// --- Fetch grid data from backend
|
||||
async loadGridData() {
|
||||
try {
|
||||
const records = await this.orm.call("samashti.board", "get_stock_moves_data", [
|
||||
|
|
@ -141,182 +92,88 @@ export class SamashtiDashboard extends Component {
|
|||
console.error("Error loading data:", error);
|
||||
}
|
||||
}
|
||||
async loadSaleData(){
|
||||
debugger;
|
||||
try {
|
||||
const data = await this.orm.call("samashti.board", "get_sale_margin_data",[
|
||||
this.state.saleFromDate,
|
||||
this.state.saleToDate
|
||||
]);
|
||||
this.state.sale_rows = data || []
|
||||
this.renderSaleGrid();
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
async export_Excel() {
|
||||
try {
|
||||
if ($(this.gridSaleContainer.el).length > 0) {
|
||||
const grid = $(this.gridSaleContainer.el).pqGrid('instance');
|
||||
const colModel = grid.option('colModel');
|
||||
const dataModel = grid.option('dataModel');
|
||||
const gridData = dataModel.data || dataModel;
|
||||
|
||||
const response = await fetch('/sale_margin/report_excel', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
colModel: colModel,
|
||||
gridData: gridData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'Sale_Margin_Report.xlsx';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
alert('Export failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Define grid columns
|
||||
async getColumns() {
|
||||
return [
|
||||
{ title: "Product Code", dataIndx: "product_code", width: 100,filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Product Name", dataIndx: "product_name", width: 280,filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Product Code", dataIndx: "product_code", width: 100, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Product Name", dataIndx: "product_name", width: 280, filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Category", dataIndx: "category", width: 150 },
|
||||
{ title: "Opening Stock", dataIndx: "opening_stock", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Receipts", dataIndx: "receipts", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Production", dataIndx: "production", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Consumption", dataIndx: "consumption", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Dispatch", dataIndx: "dispatch", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Closing Stock", dataIndx: "closing_stock", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Uom", dataIndx: "uom", width: 90, dataType:"text" },
|
||||
{ title: "Value", dataIndx: "value", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
|
||||
{ title: "Opening Stock", dataIndx: "opening_stock", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Receipts", dataIndx: "receipts", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Production", dataIndx: "production", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Consumption", dataIndx: "consumption", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Dispatch", dataIndx: "dispatch", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Closing Stock", dataIndx: "closing_stock", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
{ title: "Uom", dataIndx: "uom", width: 90, dataType: "text" },
|
||||
{ title: "Value", dataIndx: "value", width: 120, dataType: "float", format: "#,###.00", summary: { type: "sum" }, align: "right" },
|
||||
];
|
||||
}
|
||||
async getSaleColumns(){
|
||||
return[
|
||||
{ title: "Sale Order", dataIndx: "sale_order", width: 100,filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Date", dataIndx: "date", width: 150 },
|
||||
{ title: "Invoice", dataIndx: "invoice", width: 180,filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Customer", dataIndx: "customer", width: 280,filter: { type: 'textbox', condition: 'contain', listeners: ['keyup'] } },
|
||||
{ title: "Tags", dataIndx:"tags", width:100, },
|
||||
{ title: "Quantity", dataIndx: "quantity", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Weight", dataIndx: "weight", width: 150 },
|
||||
{ title: "Production Cost", dataIndx: "cost", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Sale Price", dataIndx: "sale_price", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Margin", dataIndx: "margin", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{ title: "Margin %", dataIndx: "margin_percent", width: 120, dataType: "float", format: "#,###.00",summary: { type: "sum" },align: "right" },
|
||||
{
|
||||
title: "View",
|
||||
width: 120,
|
||||
editable: false,
|
||||
summary:false,
|
||||
render: function (ui) {
|
||||
return "<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 columns = await this.getColumns();
|
||||
const agg = pq.aggregate;
|
||||
|
||||
const gridOptions = {
|
||||
selectionModel: { type: "row" },
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
editable: false,
|
||||
stripeRows: true,
|
||||
filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 },
|
||||
dataModel: { data: this.state.rows, location: "local", sorting: "local", paging: "local" },
|
||||
colModel: columns,
|
||||
toolbar: {
|
||||
items: [
|
||||
{
|
||||
type: 'select',
|
||||
label: '<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 #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"',
|
||||
listener: () => this.exportGrid() },
|
||||
|
||||
{
|
||||
type: 'button',
|
||||
icon: 'ui-icon-print',
|
||||
label: 'Print',
|
||||
attr: 'style="padding: 8px 16px; border: 1px solid #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"',
|
||||
listener: function() {
|
||||
var exportHtml = this.exportData({
|
||||
title: 'jQuery grid',
|
||||
format: 'htm',
|
||||
render: true
|
||||
}),
|
||||
newWin = window.open('', '', 'width=1200, height=700'),
|
||||
doc = newWin.document.open();
|
||||
doc.write(exportHtml);
|
||||
doc.close();
|
||||
newWin.print();
|
||||
}
|
||||
const gridOptions = {
|
||||
selectionModel: { type: "row" },
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
editable: false,
|
||||
stripeRows: true,
|
||||
filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 },
|
||||
dataModel: { data: this.state.rows, location: "local", sorting: "local", paging: "local" },
|
||||
colModel: columns,
|
||||
toolbar: {
|
||||
items: [
|
||||
{
|
||||
type: 'select',
|
||||
label: '<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 #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"',
|
||||
listener: () => this.exportGrid()
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
icon: 'ui-icon-print',
|
||||
label: 'Print',
|
||||
attr: 'style="padding: 8px 16px; border: 1px solid #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"',
|
||||
listener: function () {
|
||||
var exportHtml = this.exportData({
|
||||
title: 'Stock Report',
|
||||
format: 'htm',
|
||||
render: true
|
||||
}),
|
||||
newWin = window.open('', '', 'width=1200, height=700'),
|
||||
doc = newWin.document.open();
|
||||
doc.write(exportHtml);
|
||||
doc.close();
|
||||
newWin.print();
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
detailModel: {
|
||||
detailModel: {
|
||||
cache: false,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize PQGrid without groupModel
|
||||
var grid = $(this.gridRef.el).pqGrid()
|
||||
if (grid.length){
|
||||
var grid = $(this.gridRef.el).pqGrid()
|
||||
if (grid.length) {
|
||||
grid.pqGrid("destroy");
|
||||
}
|
||||
|
||||
$(this.gridRef.el)
|
||||
.css({ height: '600px', width: '100%' })
|
||||
.pqGrid(gridOptions);
|
||||
$(this.gridRef.el)
|
||||
.css({ height: '600px', width: '100%' })
|
||||
.pqGrid(gridOptions);
|
||||
|
||||
// Then set groupModel using groupOption
|
||||
const groupModel = {
|
||||
const groupModel = {
|
||||
on: true,
|
||||
checkbox: true,
|
||||
checkboxHead: true,
|
||||
header:true,
|
||||
header: true,
|
||||
cascade: true,
|
||||
titleInFirstCol: true,
|
||||
fixCols: true,
|
||||
|
|
@ -327,89 +184,15 @@ export class SamashtiDashboard extends Component {
|
|||
"{0} - {1}"
|
||||
]
|
||||
};
|
||||
$(this.gridRef.el).pqGrid("groupOption", groupModel);
|
||||
|
||||
// Refresh grid to apply grouping
|
||||
$(this.gridRef.el).pqGrid("refreshDataAndView");
|
||||
}
|
||||
async renderSaleGrid(){
|
||||
const columns = await this.getSaleColumns()
|
||||
const agg = pq.aggregate;
|
||||
|
||||
const gridOptions = {
|
||||
selectionModel: { type: "row" },
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
editable: false,
|
||||
postRenderInterval: -1,
|
||||
stripeRows: true,
|
||||
filterModel: { on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1 },
|
||||
dataModel: { data: this.state.sale_rows, location: "local", sorting: "local", paging: "local" },
|
||||
colModel: columns,
|
||||
toolbar: {
|
||||
items: [
|
||||
{ type: "button", label: "Export", icon: "ui-icon-arrowthickstop-1-s",
|
||||
attr: 'style="padding: 8px 16px; border: 1px solid #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"',
|
||||
listener: () => this.export_Excel() },
|
||||
|
||||
{
|
||||
type: 'button',
|
||||
icon: 'ui-icon-print',
|
||||
label: 'Print',
|
||||
attr: 'style="padding: 8px 16px; border: 1px solid #3498db; border-radius: 6px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: 600; font-size: 13px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);"',
|
||||
listener: function() {
|
||||
var exportHtml = this.exportData({
|
||||
title: 'jQuery grid',
|
||||
format: 'htm',
|
||||
render: true
|
||||
}),
|
||||
newWin = window.open('', '', 'width=1200, height=700'),
|
||||
doc = newWin.document.open();
|
||||
doc.write(exportHtml);
|
||||
doc.close();
|
||||
newWin.print();
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
detailModel: {
|
||||
cache: false,
|
||||
}
|
||||
};
|
||||
$(this.gridSaleContainer.el)
|
||||
.css({ height: '600px', width: '100%' })
|
||||
.pqGrid(gridOptions);
|
||||
// Then set groupModel using groupOption
|
||||
const groupModel = {
|
||||
on: true,
|
||||
dataIndx: ['tags'],
|
||||
checkbox: true,
|
||||
checkboxHead: true,
|
||||
header:true,
|
||||
cascade: true,
|
||||
titleInFirstCol: true,
|
||||
fixCols: true,
|
||||
showSummary: [true, true],
|
||||
grandSummary: true,
|
||||
title: [
|
||||
"{0} ({1})",
|
||||
"{0} - {1}"
|
||||
]
|
||||
};
|
||||
$(this.gridSaleContainer.el).pqGrid("groupOption", groupModel);
|
||||
|
||||
// Refresh grid to apply grouping
|
||||
$(this.gridSaleContainer.el).pqGrid("refreshDataAndView");
|
||||
}
|
||||
$(this.gridRef.el).pqGrid("groupOption", groupModel);
|
||||
$(this.gridRef.el).pqGrid("refreshDataAndView");
|
||||
}
|
||||
|
||||
exportGrid() {
|
||||
const format_ex = $("#export_format").val();
|
||||
const grid = $(this.gridRef.el);
|
||||
|
||||
console.log("Starting export with format:", format_ex);
|
||||
|
||||
// Different handling for different formats
|
||||
switch(format_ex) {
|
||||
switch (format_ex) {
|
||||
case 'xlsx':
|
||||
const blobXlsx = grid.pqGrid("exportData", {
|
||||
format: 'xlsx',
|
||||
|
|
@ -423,7 +206,6 @@ export class SamashtiDashboard extends Component {
|
|||
format: 'csv',
|
||||
render: true
|
||||
});
|
||||
// CSV often returns as string
|
||||
if (typeof blobCsv === 'string') {
|
||||
saveAs(new Blob([blobCsv], { type: 'text/csv' }), "StockData.csv");
|
||||
} else {
|
||||
|
|
@ -462,6 +244,4 @@ export class SamashtiDashboard extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
registry.category("actions").add("SamashtiDashboard", SamashtiDashboard);
|
||||
|
||||
registry.category("actions").add("StockDashboard", StockDashboard);
|
||||
|
|
|
|||
|
|
@ -1,134 +1,73 @@
|
|||
<?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 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-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>
|
||||
</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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Sale Dashboard Card -->
|
||||
<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'"/>
|
||||
</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 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>
|
||||
|
||||
<!-- 🔹 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">
|
||||
<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 class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<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>
|
||||
</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 class="card-body" style="padding: 0;">
|
||||
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue