[IMP] system_dashboard_classic: automatic update

Auto-generated commit based on local changes.
This commit is contained in:
maltayyar2 2025-12-28 15:24:22 +03:00
parent 8617de2c7e
commit 3c3a32a266
5 changed files with 313 additions and 28 deletions

View File

@ -567,16 +567,22 @@ class DashboardConfigSettings(models.TransientModel):
], default='donut', string='Attendance Hours Chart',
config_parameter='system_dashboard_classic.attendance_hours_chart_type')
@api.model
def get_dashboard_colors(self):
"""API method to get dashboard colors for JavaScript"""
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'),
}
# Periodic Refresh Settings
dashboard_refresh_enabled = fields.Boolean(
string='Enable Auto Refresh',
default=False,
config_parameter='system_dashboard_classic.refresh_enabled',
help='Automatically refresh attendance status and approval count at regular intervals'
)
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
def get_values(self):

View File

@ -638,8 +638,29 @@ class SystemDashboard(models.Model):
except (json.JSONDecodeError, TypeError, AttributeError):
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
@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):
"""
Calculate the great-circle distance between two GPS points using Haversine formula.
@ -879,3 +900,124 @@ class SystemDashboard(models.Model):
if order_type:
return all_orders.get(order_type, [])
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

View File

@ -123,33 +123,40 @@ odoo.define('system_dashboard_classic.genius_enhancements', function(require) {
* Generates light/dark variants automatically for complete theming
*/
function loadCustomColors() {
var ICP = 'ir.config_parameter';
var prefix = 'system_dashboard_classic.';
var DashboardModel = 'system_dashboard_classic.dashboard';
// Define all color parameters with their CSS variable names
var colorParams = [
{ param: 'primary_color', cssVar: '--dash-primary', defaultColor: '#0891b2' },
{ param: 'secondary_color', cssVar: '--dash-secondary', defaultColor: '#1e293b' },
{ param: 'success_color', cssVar: '--dash-success', defaultColor: '#10b981' },
{ param: 'warning_color', cssVar: '--dash-warning', defaultColor: '#f59e0b' }
{ param: 'primary', cssVar: '--dash-primary', defaultColor: '#0891b2' },
{ param: 'secondary', cssVar: '--dash-secondary', defaultColor: '#1e293b' },
{ param: 'success', cssVar: '--dash-success', defaultColor: '#10b981' },
{ param: 'warning', cssVar: '--dash-warning', defaultColor: '#f59e0b' }
];
// Load each color
colorParams.forEach(function(item) {
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: ICP,
method: 'get_param',
args: [prefix + item.param],
kwargs: {}
}).then(function(color) {
if (color && color.match(/^#[0-9A-Fa-f]{6}$/)) {
applyColorWithVariants(item.cssVar, color);
// Fetch valid dashboard colors from the secure public method
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: DashboardModel,
method: 'get_public_dashboard_colors',
args: [],
kwargs: {}
}).then(function(colors) {
if (!colors) return;
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
if (item.cssVar === '--dash-primary') {
generateIconFilter(color);
generateIconFilter(colorValue);
}
}
}).catch(function() {});
});
}).catch(function(err) {
console.warn('[Genius Enhancements] Failed to load custom colors:', err);
});
}

View File

@ -202,6 +202,11 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
}, []).then(function(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
// This prevents the "flash of green/default" issue by setting CSS vars synchronously
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
setTimeout(function() {
window.check = false;
@ -1564,6 +1655,12 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
clearInterval(this.pollingIntervalId);
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
this._super.apply(this, arguments);
// Clear attendance clock interval if exists

View File

@ -200,6 +200,39 @@
</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>
</xpath>
</field>