[IMP] system_dashboard_classic: automatic update
Auto-generated commit based on local changes.
This commit is contained in:
parent
8617de2c7e
commit
3c3a32a266
|
|
@ -567,16 +567,22 @@ class DashboardConfigSettings(models.TransientModel):
|
||||||
], default='donut', string='Attendance Hours Chart',
|
], default='donut', string='Attendance Hours Chart',
|
||||||
config_parameter='system_dashboard_classic.attendance_hours_chart_type')
|
config_parameter='system_dashboard_classic.attendance_hours_chart_type')
|
||||||
|
|
||||||
@api.model
|
# Periodic Refresh Settings
|
||||||
def get_dashboard_colors(self):
|
dashboard_refresh_enabled = fields.Boolean(
|
||||||
"""API method to get dashboard colors for JavaScript"""
|
string='Enable Auto Refresh',
|
||||||
ICPSudo = self.env['ir.config_parameter'].sudo()
|
default=False,
|
||||||
return {
|
config_parameter='system_dashboard_classic.refresh_enabled',
|
||||||
'primary': ICPSudo.get_param('system_dashboard_classic.primary_color', '#0891b2'),
|
help='Automatically refresh attendance status and approval count at regular intervals'
|
||||||
'secondary': ICPSudo.get_param('system_dashboard_classic.secondary_color', '#1e293b'),
|
)
|
||||||
'success': ICPSudo.get_param('system_dashboard_classic.success_color', '#10b981'),
|
|
||||||
'warning': ICPSudo.get_param('system_dashboard_classic.warning_color', '#f59e0b'),
|
dashboard_refresh_interval = fields.Integer(
|
||||||
}
|
string='Refresh Interval (seconds)',
|
||||||
|
default=60,
|
||||||
|
config_parameter='system_dashboard_classic.refresh_interval',
|
||||||
|
help='How often to refresh data (minimum: 30 seconds, maximum: 3600 seconds / 1 hour)'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def get_values(self):
|
def get_values(self):
|
||||||
|
|
|
||||||
|
|
@ -638,8 +638,29 @@ class SystemDashboard(models.Model):
|
||||||
except (json.JSONDecodeError, TypeError, AttributeError):
|
except (json.JSONDecodeError, TypeError, AttributeError):
|
||||||
values['card_orders'] = {}
|
values['card_orders'] = {}
|
||||||
|
|
||||||
|
# Load periodic refresh settings
|
||||||
|
ICP = self.env['ir.config_parameter'].sudo()
|
||||||
|
values['refresh_settings'] = {
|
||||||
|
'enabled': ICP.get_param('system_dashboard_classic.refresh_enabled', 'False') == 'True',
|
||||||
|
'interval': int(ICP.get_param('system_dashboard_classic.refresh_interval', '60'))
|
||||||
|
}
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_public_dashboard_colors(self):
|
||||||
|
"""
|
||||||
|
Secure API method to get dashboard colors for Genius Enhancements JS.
|
||||||
|
Uses sudo() to bypass access rules for ir.config_parameter.
|
||||||
|
"""
|
||||||
|
ICPSudo = self.env['ir.config_parameter'].sudo()
|
||||||
|
return {
|
||||||
|
'primary': ICPSudo.get_param('system_dashboard_classic.primary_color', '#0891b2'),
|
||||||
|
'secondary': ICPSudo.get_param('system_dashboard_classic.secondary_color', '#1e293b'),
|
||||||
|
'success': ICPSudo.get_param('system_dashboard_classic.success_color', '#10b981'),
|
||||||
|
'warning': ICPSudo.get_param('system_dashboard_classic.warning_color', '#f59e0b'),
|
||||||
|
}
|
||||||
|
|
||||||
def _haversine_distance(self, lat1, lon1, lat2, lon2):
|
def _haversine_distance(self, lat1, lon1, lat2, lon2):
|
||||||
"""
|
"""
|
||||||
Calculate the great-circle distance between two GPS points using Haversine formula.
|
Calculate the great-circle distance between two GPS points using Haversine formula.
|
||||||
|
|
@ -879,3 +900,124 @@ class SystemDashboard(models.Model):
|
||||||
if order_type:
|
if order_type:
|
||||||
return all_orders.get(order_type, [])
|
return all_orders.get(order_type, [])
|
||||||
return all_orders
|
return all_orders
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_refresh_data(self):
|
||||||
|
"""
|
||||||
|
Lightweight method for periodic refresh.
|
||||||
|
Returns only attendance status and approval count.
|
||||||
|
Does NOT return full card data for performance.
|
||||||
|
|
||||||
|
@return: Dict with attendance status and approval count
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
'attendance': [],
|
||||||
|
'approval_count': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
user = self.env['res.users'].browse(self.env.uid)
|
||||||
|
employee = self.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1)
|
||||||
|
|
||||||
|
if not employee:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ATTENDANCE LOGIC: Matches get_data exactly
|
||||||
|
# 1. Use correct model 'attendance.attendance' (Event-based)
|
||||||
|
# 2. Convert to User's Timezone
|
||||||
|
# 3. Determine status based on 'action' field
|
||||||
|
|
||||||
|
last_attendance = self.env['attendance.attendance'].sudo().search(
|
||||||
|
[('employee_id', '=', employee.id)],
|
||||||
|
order='name desc',
|
||||||
|
limit=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if last_attendance:
|
||||||
|
is_attendance = (last_attendance.action == 'sign_in')
|
||||||
|
|
||||||
|
# Timezone Conversion
|
||||||
|
user_tz = pytz.timezone(self.env.context.get('tz', 'Asia/Riyadh') or self.env.user.tz or 'UTC')
|
||||||
|
time_str = ''
|
||||||
|
|
||||||
|
if last_attendance.name:
|
||||||
|
# 'name' is the Datetime field in UTC
|
||||||
|
time_object = fields.Datetime.from_string(last_attendance.name)
|
||||||
|
# Convert to user's timezone
|
||||||
|
time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz)
|
||||||
|
# Format to string for display (removing timezone offset info for cleanliness)
|
||||||
|
time_str = time_in_timezone.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
result['attendance'] = [{
|
||||||
|
'is_attendance': is_attendance,
|
||||||
|
'time': time_str,
|
||||||
|
}]
|
||||||
|
|
||||||
|
# Calculate approval count
|
||||||
|
import ast
|
||||||
|
|
||||||
|
# Iterate over all dashboard lines (which represent "cards" or sections of cards)
|
||||||
|
# We process lines directly to be more efficient than iterating boards -> lines
|
||||||
|
config_lines = self.env['base.dashbord.line'].sudo().search([])
|
||||||
|
approval_count = 0
|
||||||
|
|
||||||
|
for line in config_lines:
|
||||||
|
# 1. User Access Check using the existing helper method
|
||||||
|
# This checks if the current user is in the allowed groups for this line
|
||||||
|
if not self.is_user(line.group_ids, user):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. Get the parent dashboard definition
|
||||||
|
dashboard = line.board_id
|
||||||
|
if not dashboard or not dashboard.model_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3. Construct Domain (Replicating logic from get_data)
|
||||||
|
domain = []
|
||||||
|
|
||||||
|
# Add Action Domain (if configured)
|
||||||
|
if dashboard.action_domain:
|
||||||
|
try:
|
||||||
|
# Parse string domain to list
|
||||||
|
dom_list = ast.literal_eval(dashboard.action_domain)
|
||||||
|
# Filter out 'state' from action domain as per get_data logic
|
||||||
|
# This prevents conflict with the specific state/stage we are checking below
|
||||||
|
for item in dom_list:
|
||||||
|
if isinstance(item, (list, tuple)) and len(item) > 0 and item[0] != 'state':
|
||||||
|
domain.append(item)
|
||||||
|
except Exception:
|
||||||
|
# Invalid domain string, ignore action domain
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Special Handling for HR Holidays Workflow
|
||||||
|
if dashboard.model_name == 'hr.holidays':
|
||||||
|
hr_holidays_workflow = self.env['ir.module.module'].sudo().search([('name', '=', 'hr_holidays_workflow')], limit=1)
|
||||||
|
if hr_holidays_workflow and hr_holidays_workflow.state == 'installed':
|
||||||
|
# Logic from get_data: OR condition for stage name matching state
|
||||||
|
domain.append('|')
|
||||||
|
domain.append(('stage_id.name', '=', line.state_id.state))
|
||||||
|
|
||||||
|
# Add State/Stage Filter
|
||||||
|
# Using the exact same logic as get_data to target the specific bucket
|
||||||
|
if line.state_id:
|
||||||
|
domain.append(('state', '=', line.state_id.state))
|
||||||
|
elif line.stage_id:
|
||||||
|
# stage_id in stage.stage is a Char field holding the ID, so we cast to int
|
||||||
|
# This matches get_data: ('stage_id', '=', int(line.stage_id.stage_id))
|
||||||
|
try:
|
||||||
|
target_stage_id = int(line.stage_id.stage_id)
|
||||||
|
domain.append(('stage_id', '=', target_stage_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4. Execute Count Query
|
||||||
|
try:
|
||||||
|
# Ensure the model exists in the registry
|
||||||
|
if dashboard.model_name in self.env:
|
||||||
|
line_count = self.env[dashboard.model_name].sudo().search_count(domain)
|
||||||
|
approval_count += line_count
|
||||||
|
except Exception:
|
||||||
|
# Failsafe: If domain is invalid or model doesn't exist, skip this line
|
||||||
|
pass
|
||||||
|
|
||||||
|
result['approval_count'] = approval_count
|
||||||
|
return result
|
||||||
|
|
|
||||||
|
|
@ -123,33 +123,40 @@ odoo.define('system_dashboard_classic.genius_enhancements', function(require) {
|
||||||
* Generates light/dark variants automatically for complete theming
|
* Generates light/dark variants automatically for complete theming
|
||||||
*/
|
*/
|
||||||
function loadCustomColors() {
|
function loadCustomColors() {
|
||||||
var ICP = 'ir.config_parameter';
|
var DashboardModel = 'system_dashboard_classic.dashboard';
|
||||||
var prefix = 'system_dashboard_classic.';
|
|
||||||
|
|
||||||
// Define all color parameters with their CSS variable names
|
// Define all color parameters with their CSS variable names
|
||||||
var colorParams = [
|
var colorParams = [
|
||||||
{ param: 'primary_color', cssVar: '--dash-primary', defaultColor: '#0891b2' },
|
{ param: 'primary', cssVar: '--dash-primary', defaultColor: '#0891b2' },
|
||||||
{ param: 'secondary_color', cssVar: '--dash-secondary', defaultColor: '#1e293b' },
|
{ param: 'secondary', cssVar: '--dash-secondary', defaultColor: '#1e293b' },
|
||||||
{ param: 'success_color', cssVar: '--dash-success', defaultColor: '#10b981' },
|
{ param: 'success', cssVar: '--dash-success', defaultColor: '#10b981' },
|
||||||
{ param: 'warning_color', cssVar: '--dash-warning', defaultColor: '#f59e0b' }
|
{ param: 'warning', cssVar: '--dash-warning', defaultColor: '#f59e0b' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Load each color
|
// Fetch valid dashboard colors from the secure public method
|
||||||
colorParams.forEach(function(item) {
|
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
|
||||||
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
|
model: DashboardModel,
|
||||||
model: ICP,
|
method: 'get_public_dashboard_colors',
|
||||||
method: 'get_param',
|
args: [],
|
||||||
args: [prefix + item.param],
|
kwargs: {}
|
||||||
kwargs: {}
|
}).then(function(colors) {
|
||||||
}).then(function(color) {
|
if (!colors) return;
|
||||||
if (color && color.match(/^#[0-9A-Fa-f]{6}$/)) {
|
|
||||||
applyColorWithVariants(item.cssVar, color);
|
colorParams.forEach(function(item) {
|
||||||
|
var colorValue = colors[item.param];
|
||||||
|
// Use default if not returned or invalid
|
||||||
|
if (!colorValue) colorValue = item.defaultColor;
|
||||||
|
|
||||||
|
if (colorValue && colorValue.match(/^#[0-9A-Fa-f]{6}$/)) {
|
||||||
|
applyColorWithVariants(item.cssVar, colorValue);
|
||||||
// For primary color, also generate SVG icon filter
|
// For primary color, also generate SVG icon filter
|
||||||
if (item.cssVar === '--dash-primary') {
|
if (item.cssVar === '--dash-primary') {
|
||||||
generateIconFilter(color);
|
generateIconFilter(colorValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(function() {});
|
});
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.warn('[Genius Enhancements] Failed to load custom colors:', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,11 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
}, []).then(function(result) {
|
}, []).then(function(result) {
|
||||||
window.resultX = result;
|
window.resultX = result;
|
||||||
|
|
||||||
|
// Load periodic refresh settings
|
||||||
|
var refreshEnabled = result.refresh_settings && result.refresh_settings.enabled;
|
||||||
|
var refreshInterval = (result.refresh_settings && result.refresh_settings.interval) || 60;
|
||||||
|
refreshInterval = Math.max(30, Math.min(3600, refreshInterval)) * 1000; // Clamp 30s-3600s, convert to ms
|
||||||
|
|
||||||
// IMMEDIATE CSS VARIABLE APPLICATION - Apply colors before any rendering
|
// IMMEDIATE CSS VARIABLE APPLICATION - Apply colors before any rendering
|
||||||
// This prevents the "flash of green/default" issue by setting CSS vars synchronously
|
// This prevents the "flash of green/default" issue by setting CSS vars synchronously
|
||||||
if (result.chart_colors) {
|
if (result.chart_colors) {
|
||||||
|
|
@ -836,6 +841,92 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// PERIODIC REFRESH - Updates attendance + approval count
|
||||||
|
// Uses Page Visibility API to pause when tab hidden
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
self.periodicRefreshId = null;
|
||||||
|
var isRtl = $('body').hasClass('o_rtl');
|
||||||
|
|
||||||
|
function updateAttendanceStatus(attendance) {
|
||||||
|
if (!attendance || attendance.length === 0) return;
|
||||||
|
|
||||||
|
var att = attendance[0];
|
||||||
|
var $imgSection = $('.attendance-img-section');
|
||||||
|
var $infoSection = $('.last-checkin-info');
|
||||||
|
|
||||||
|
// Get localized titles
|
||||||
|
var checkin_title = isRtl ? 'وقت الدخول' : 'Check-in Time';
|
||||||
|
var checkout_title = isRtl ? 'وقت الخروج' : 'Check-out Time';
|
||||||
|
|
||||||
|
if (att.is_attendance) {
|
||||||
|
// User is checked IN - show logout icon with checkout title
|
||||||
|
var image = '<img class="img-logout attendance-icon" src="/system_dashboard_classic/static/src/icons/logout.svg"/>';
|
||||||
|
$imgSection.html(image).removeClass('state-checkin').addClass('state-checkout');
|
||||||
|
|
||||||
|
// Show check-in info
|
||||||
|
var checkinInfoHtml = '<div class="checkin-label">' + checkout_title + '</div>' +
|
||||||
|
'<div class="checkin-time">' + att.time + '</div>';
|
||||||
|
$infoSection.html(checkinInfoHtml).show();
|
||||||
|
} else {
|
||||||
|
// User is checked OUT - show login icon with checkin title
|
||||||
|
var image = '<img class="img-login attendance-icon" src="/system_dashboard_classic/static/src/icons/login.svg"/>';
|
||||||
|
$imgSection.html(image).removeClass('state-checkout').addClass('state-checkin');
|
||||||
|
|
||||||
|
// Show check-out info if time exists
|
||||||
|
if (att.time) {
|
||||||
|
var checkoutInfoHtml = '<div class="checkin-label checkout-label">' + checkin_title + '</div>' +
|
||||||
|
'<div class="checkin-time checkout-time">' + att.time + '</div>';
|
||||||
|
$infoSection.html(checkoutInfoHtml).show();
|
||||||
|
} else {
|
||||||
|
$infoSection.html('').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateApprovalCount(count) {
|
||||||
|
var $badge = $('.pending-count-badge');
|
||||||
|
if (count > 0) {
|
||||||
|
$badge.text(count).show();
|
||||||
|
} else {
|
||||||
|
$badge.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPeriodicRefresh() {
|
||||||
|
if (!refreshEnabled || refreshInterval <= 0) {
|
||||||
|
console.log('[Dashboard] Periodic refresh disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Dashboard] Starting periodic refresh (interval: ' + (refreshInterval/1000) + 's)');
|
||||||
|
|
||||||
|
self.periodicRefreshId = setInterval(function() {
|
||||||
|
// Skip if page is hidden (browser tab switch)
|
||||||
|
if (document.hidden) {
|
||||||
|
console.log('[Dashboard] Skipping refresh - tab hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch lightweight data
|
||||||
|
self._rpc({
|
||||||
|
model: 'system_dashboard_classic.dashboard',
|
||||||
|
method: 'get_refresh_data',
|
||||||
|
}, []).then(function(data) {
|
||||||
|
updateAttendanceStatus(data.attendance);
|
||||||
|
updateApprovalCount(data.approval_count);
|
||||||
|
console.log('[Dashboard] Periodic refresh completed - Count: ' + data.approval_count);
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.warn('[Dashboard] Refresh failed:', err);
|
||||||
|
});
|
||||||
|
}, refreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
startPeriodicRefresh();
|
||||||
|
|
||||||
// Chart Settings
|
// Chart Settings
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
window.check = false;
|
window.check = false;
|
||||||
|
|
@ -1564,6 +1655,12 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
clearInterval(this.pollingIntervalId);
|
clearInterval(this.pollingIntervalId);
|
||||||
this.pollingIntervalId = null;
|
this.pollingIntervalId = null;
|
||||||
}
|
}
|
||||||
|
// Clear periodic refresh interval
|
||||||
|
if (this.periodicRefreshId) {
|
||||||
|
clearInterval(this.periodicRefreshId);
|
||||||
|
this.periodicRefreshId = null;
|
||||||
|
console.log('[Dashboard] Periodic refresh stopped');
|
||||||
|
}
|
||||||
// Call parent destroy
|
// Call parent destroy
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
// Clear attendance clock interval if exists
|
// Clear attendance clock interval if exists
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,39 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Periodic Refresh Settings -->
|
||||||
|
<h2 class="mt32">Auto-Refresh Settings</h2>
|
||||||
|
<div class="row mt16 o_settings_container">
|
||||||
|
<div class="col-12 col-lg-6 o_setting_box">
|
||||||
|
<div class="o_setting_left_pane">
|
||||||
|
<field name="dashboard_refresh_enabled" widget="boolean_toggle"/>
|
||||||
|
</div>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<label for="dashboard_refresh_enabled" string="Enable Auto Refresh"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
Automatically refresh attendance status and approval count at regular intervals.
|
||||||
|
<br/><em>Useful when employees use mobile app for check-in/out.</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-6 o_setting_box"
|
||||||
|
attrs="{'invisible': [('dashboard_refresh_enabled', '=', False)]}">
|
||||||
|
<div class="o_setting_left_pane">
|
||||||
|
<i class="fa fa-clock-o fa-3x"/>
|
||||||
|
</div>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<label for="dashboard_refresh_interval" string="Refresh Interval"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
How often to refresh data (in seconds)
|
||||||
|
<br/><em>Range: 30 seconds to 3600 seconds (1 hour)</em>
|
||||||
|
</div>
|
||||||
|
<div class="content-group mt8">
|
||||||
|
<field name="dashboard_refresh_interval" class="oe_inline"/> seconds
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue