[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',
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
// Fetch valid dashboard colors from the secure public method
|
||||
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
|
||||
model: ICP,
|
||||
method: 'get_param',
|
||||
args: [prefix + item.param],
|
||||
model: DashboardModel,
|
||||
method: 'get_public_dashboard_colors',
|
||||
args: [],
|
||||
kwargs: {}
|
||||
}).then(function(color) {
|
||||
if (color && color.match(/^#[0-9A-Fa-f]{6}$/)) {
|
||||
applyColorWithVariants(item.cssVar, color);
|
||||
}).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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue