vanilla 19.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:49:46 +02:00
parent 991d2234ca
commit d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions

View file

@ -12,48 +12,48 @@
<field name="external_report_layout_id" invisible="1" />
<field name="logo_primary_color" invisible="1" />
<field name="logo_secondary_color" invisible="1" />
<field name="report_layout_id" widget="radio" string="Layout" required="1"/>
<field name="font" widget="selection" required="1"/>
<field name="logo" widget="image" options="{'size': [0, 50]}"/>
<label for="primary_color" string="Colors" />
<div class="o_document_layout_colors">
<field name="primary_color" widget="color"/>
<field name="secondary_color" widget="color"/>
<div class="o_custom_colors" title="Reset to logo colors"
attrs="{'invisible': [('custom_colors', '=', False)]}">
<span class="fa fa-refresh fa-2x"></span>
<field name="custom_colors" nolabel="1"/>
</div>
<field name="report_layout_id" string="Layout" widget="selection_badge" required="1" options="{'horizontal': true, 'size': 'sm'}"/>
<field name="layout_background" string="Background" widget="selection" required="1"/>
<field name="layout_background_image" options="{'accepted_file_extensions': 'image/*'}" invisible="layout_background != 'Custom'" required="layout_background == 'Custom'">Upload your file</field>
<field name="font" string="Text" widget="selection" required="1"/>
<field name="logo" string="Logo" widget="image" options="{'size': [0, 50]}"/>
<label for="primary_color" string="Colors"/>
<div class="o_document_layout_colors d-flex align-items-end mb-4">
<field name="primary_color" widget="color" class="w-auto m-0 me-1"/>
<field name="secondary_color" widget="color" class="w-auto m-0"/>
<a class="o_custom_colors btn btn-secondary btn-sm position-relative ms-2" role="button" title="Reset to logo colors" invisible="not custom_colors">
<i class="fa fa-repeat"/> Reset
<field name="custom_colors" class="position-absolute top-0 start-0 w-100 h-100 opacity-0" nolabel="1"/>
</a>
</div>
<field name="layout_background" widget="selection" required="1"/>
<field name="layout_background_image" options="{'accepted_file_extensions': 'image/*'}" attrs="{'invisible': [('layout_background', '!=', 'Custom')], 'required': [('layout_background', '=', 'Custom')]}">Upload your file</field>
<field name="report_header" placeholder="e.g. Global Business Solutions" options="{'resizable': false}"/>
<field name="company_details" string="Company Details" options="{'resizable': false}"/>
<field name="report_footer" string="Footer" options="{'resizable': false}"/>
<field name="paperformat_id" widget="selection" required="1" domain="[('report_ids', '=', False)]"/>
<field name="company_details" string="Address" options="{'resizable': false}"/>
<field name="report_header" string="Tagline" placeholder="e.g. Global Business Solutions" options="{'resizable': false}"/>
<field name="report_footer" placeholder="Write your phone, email, bank account, tax ID, ..." string="Footer" options="{'resizable': false}"/>
<field name="paperformat_id" widget="selection" required="1"/>
</group>
<div>
<field name="preview" widget="iframe_wrapper" />
<button name="web.action_report_layout_preview" string="Download PDF Preview" type="action" class="oe_link" icon="fa-arrow-right"/>
<div class="o_preview">
<field name="preview" widget="iframe_wrapper" class="preview_document_layout d-flex justify-content-center mb-0"/>
</div>
</group>
<footer>
<button string="Save" class="btn-primary" type="object" name="document_layout_save" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Cancel" />
<button string="Continue" class="btn-primary" type="object" name="document_layout_save" data-hotkey="q"/>
<button special="cancel" data-hotkey="x" string="Discard" />
</footer>
</form>
</field>
</record>
<record id="action_base_document_layout_configurator" model="ir.actions.act_window">
<field name="type">ir.actions.act_window</field>
<field name="name">Configure your document layout</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="res_model">base.document.layout</field>
<field name="view_id" ref="web.view_base_document_layout"/>
<field name="context">{"dialog_size": "extra-large"}</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_view_form_inherit_view" model="ir.ui.view">
<field name="name">ir.ui.view.form.inherit</field>
<field name="model">ir.ui.view</field>
<field name="inherit_id" ref="base.view_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('alert-info')]" position="after">
<div class="alert alert-danger" role="alert" invisible="not invalid_locators">
Please note that your view includes invalid locators.<br/>
These nodes could not be anchored to the parent view and have no effect.<br/>
This issue may have arisen as a result of manual modifications or during the upgrade process.<br/>
For your reference, invalid xpath nodes are highlighted in red.
</div>
<field name="invalid_locators" invisible="1"/> <!-- required for the alert -->
</xpath>
<xpath expr="//field[@name='arch_base']" position="attributes">
<attribute name="widget">code_ir_ui_view</attribute>
</xpath>
</field>
</record>
</odoo>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend_legacy_lazy" name="Lazy assets for legacy Views" groups="base.group_user">
<t t-call-assets="web.assets_backend_legacy_lazy" />
</template>
</odoo>

View file

@ -0,0 +1,393 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="view_memory">
&lt;!DOCTYPE html&gt;
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Memory Usage Visualization</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>
<style>
/* General body and layout styles */
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f9f9f9;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
/* Row definitions for the new layout */
.top-row {
height: 40vh; /* Line graph takes 40% of the viewport height */
}
.bottom-row {
display: flex;
gap: 20px;
/* The height of this row will be determined by its content */
}
/* Chart container styles */
.chart-container {
position: relative;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
height: 100%;
width: 100%;
}
/* Specific styling for the bottom row elements */
.bar-chart-wrapper {
flex: 2; /* Bar chart takes 2/3 of the width */
min-height: 400px; /* Minimum height for the bar chart */
}
.traceback-panel {
flex: 1; /* Traceback panel takes 1/3 of the width */
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
overflow-y: auto;
min-height: 400px; /* Match min-height for alignment */
height: fit-content; /* Adjust height to content */
}
/* Traceback panel content styling */
.traceback-header {
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.traceback-item {
background: white;
margin: 5px 0;
padding: 8px;
border-radius: 3px;
border-left: 3px solid #007acc;
font-family: monospace;
font-size: 12px;
}
.traceback-file {
color: #007acc;
font-weight: bold;
}
.copyable {
cursor: pointer;
user-select: all;
padding: 2px 4px;
border-radius: 3px;
transition: background-color 0.2s;
}
.copyable:hover {
background-color: #e8f4f8;
}
.traceback-line {
color: #666;
}
.no-selection {
color: #999;
font-style: italic;
text-align: center;
padding-top: 50px;
}
</style>
</head>
<body>
<div class="container">
<!-- Top row for the full-width line chart -->
<div class="top-row">
<div class="chart-container">
<canvas id="totalMemoryChart"></canvas>
</div>
</div>
<!-- Bottom row for the bar chart and traceback panel -->
<div class="bottom-row">
<div class="bar-chart-wrapper chart-container" id="memoryChartContainer">
<canvas id="memoryChart"></canvas>
</div>
<div class="traceback-panel" id="tracebackPanel">
<div class="no-selection">Click a point on the top chart, then a bar below to see details.</div>
</div>
</div>
</div>
<script>
// Get data from Odoo template
const rawData = JSON.parse(atob('<t t-esc="memory_graph"/>'));
// --- UTILITY FUNCTIONS ---
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
const value = bytes / Math.pow(k, i);
return `${value.toFixed(1)} ${sizes[i]}`;
}
function formatTimestamp(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
function getLastFileFromTraceback(traceback) {
if (!traceback || traceback.length === 0) return "unknown:0";
const lastFrame = traceback[traceback.length - 1];
return `${lastFrame[0]}:${lastFrame[1]}`;
}
function getFullTracebackKey(traceback) {
if (!traceback || traceback.length === 0) return "unknown";
return traceback.map(frame => `${frame[0]}:${frame[1]}`).join(' -> ');
}
// --- DYNAMIC UI UPDATES ---
function updateChartHeight(chart, dataLength) {
const container = document.getElementById('memoryChartContainer');
if (dataLength === 0) {
container.style.height = '400px'; // Reset to min-height
chart.resize();
return;
}
const minBarHeight = 35; // Minimum height per bar for readability
const padding = 100; // Extra space for axes and labels
const newHeight = Math.max(400, (dataLength * minBarHeight) + padding);
container.style.height = newHeight + 'px';
chart.resize();
}
function showTraceback(traceback, size, fullTracebackKey) {
const panel = document.getElementById('tracebackPanel');
if (!traceback || traceback.length === 0) {
panel.innerHTML = '<div class="no-selection">No traceback available</div>';
return;
}
let html = `<div class="traceback-header">Traceback (${formatBytes(size)})</div>`;
if (fullTracebackKey) {
html += `
<div style="margin-bottom: 10px; padding: 8px; background: #e8f4f8; border-radius: 3px; font-size: 11px; color: #555;">
<strong>Full Path:</strong> <span class="copyable" title="Click to select for copying">${fullTracebackKey}</span>
</div>
`;
}
traceback.forEach(frame => {
const [filename, lineNo, functionName, code] = frame;
html += `
<div class="traceback-item">
<div class="traceback-file copyable" title="Click to select for copying">${filename}:${lineNo}</div>
<div>in <strong>${functionName || 'unknown'}</strong></div>
${code ? `<div class="traceback-line">${code}</div>` : ''}
</div>
`;
});
panel.innerHTML = html;
}
// --- CHART LOGIC ---
const totalSizes = rawData.map(entry =>
entry.samples.reduce((sum, sample) => sum + sample.size, 0)
);
const lineLabels = rawData.map(entry => formatTimestamp(entry.start));
let selectedIndexes = [];
let currentBreakdownData = [];
const updateBarChart = () => {
if (selectedIndexes.length === 1) {
const entry = rawData[selectedIndexes[0]];
const groupedData = {};
entry.samples.forEach(sample => {
const key = getLastFileFromTraceback(sample.traceback);
if (!groupedData[key]) {
groupedData[key] = { size: 0, traceback: sample.traceback, samples: [] };
}
groupedData[key].size += sample.size;
groupedData[key].samples.push(sample);
});
currentBreakdownData = Object.entries(groupedData).map(([key, data]) => ({
label: key,
size: data.size,
traceback: data.traceback,
samples: data.samples
})).sort((a, b) => b.size - a.size);
breakdownChart.data.labels = currentBreakdownData.map(item => item.label);
breakdownChart.data.datasets = [{
label: `Memory Usage`,
data: currentBreakdownData.map(item => item.size),
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}];
breakdownChart.options.plugins.title.text = `Memory Breakdown for ${formatTimestamp(entry.start)}`;
} else if (selectedIndexes.length === 2) {
const [idx1, idx2] = selectedIndexes.sort((a, b) => a - b);
const entry1 = rawData[idx1];
const entry2 = rawData[idx2];
const groupDataByFullTraceback = (entry) => {
const grouped = {};
entry.samples.forEach(sample => {
const key = getFullTracebackKey(sample.traceback);
if (!grouped[key]) {
grouped[key] = { size: 0, traceback: sample.traceback, lastFile: getLastFileFromTraceback(sample.traceback) };
}
grouped[key].size += sample.size;
});
return grouped;
};
const data1 = groupDataByFullTraceback(entry1);
const data2 = groupDataByFullTraceback(entry2);
const labelSet = new Set([...Object.keys(data1), ...Object.keys(data2)]);
let diffData = [];
labelSet.forEach(fullTracebackKey => {
const size1 = data1[fullTracebackKey]?.size || 0;
const size2 = data2[fullTracebackKey]?.size || 0;
const diff = size2 - size1;
if (diff !== 0) {
const item = data2[fullTracebackKey] || data1[fullTracebackKey];
diffData.push({
label: item.lastFile,
diff: diff,
traceback: item.traceback,
fullTracebackKey: fullTracebackKey
});
}
});
currentBreakdownData = diffData.sort((a, b) => b.diff - a.diff);
breakdownChart.data.labels = currentBreakdownData.map(item => item.label);
breakdownChart.data.datasets = [{
label: `Difference`,
data: currentBreakdownData.map(item => item.diff),
backgroundColor: item => item.raw >= 0 ? 'rgba(75, 192, 192, 0.6)' : 'rgba(255, 99, 132, 0.6)',
borderColor: item => item.raw >= 0 ? 'rgba(75, 192, 192, 1)' : 'rgba(255, 99, 132, 1)',
borderWidth: 1
}];
breakdownChart.options.plugins.title.text = `Memory Difference: ${formatTimestamp(entry2.start)} vs ${formatTimestamp(entry1.start)}`;
} else {
currentBreakdownData = [];
breakdownChart.data.labels = [];
breakdownChart.data.datasets = [];
breakdownChart.options.plugins.title.text = 'Memory Breakdown';
document.getElementById('tracebackPanel').innerHTML =
'<div class="no-selection">Click a point on the top chart, then a bar below to see details.</div>';
}
updateChartHeight(breakdownChart, currentBreakdownData.length);
breakdownChart.update();
totalChart.update();
};
// --- CHART INSTANTIATION ---
const totalChart = new Chart(document.getElementById('totalMemoryChart'), {
type: 'line',
data: {
labels: lineLabels,
datasets: [{
label: 'Total Memory Usage',
data: totalSizes,
fill: false,
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.1,
pointBackgroundColor: ctx => selectedIndexes.includes(ctx.dataIndex) ? 'rgba(255, 99, 132, 1)' : 'rgba(75, 192, 192, 1)',
pointRadius: ctx => selectedIndexes.includes(ctx.dataIndex) ? 7 : 4,
pointBorderWidth: ctx => selectedIndexes.includes(ctx.dataIndex) ? 3 : 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: { display: true, text: 'Total Memory Usage Over Time (Click to select, Shift+Click to compare)' },
tooltip: { callbacks: { label: context => `Total: ${formatBytes(context.raw)}` } }
},
onClick: (evt, elements) => {
if (elements.length > 0) {
const idx = elements[0].index;
const isSelected = selectedIndexes.includes(idx);
if (evt.native.shiftKey) {
if (isSelected) {
selectedIndexes = selectedIndexes.filter(i => i !== idx);
} else {
selectedIndexes.push(idx);
if (selectedIndexes.length > 2) selectedIndexes.shift();
}
} else {
selectedIndexes = isSelected &amp;&amp; selectedIndexes.length === 1 ? [] : [idx];
}
} else {
selectedIndexes = [];
}
updateBarChart();
},
scales: {
y: { beginAtZero: true, ticks: { callback: formatBytes }, title: { display: true, text: 'Total Memory' } },
x: { title: { display: true, text: 'Time' } }
}
}
});
const breakdownChart = new Chart(document.getElementById('memoryChart'), {
type: 'bar',
data: { labels: [], datasets: [] },
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
layout: { padding: { right: 120 } },
plugins: {
legend: { display: false },
title: { display: true, text: 'Memory Breakdown' },
tooltip: { callbacks: { label: context => `${formatBytes(context.raw)}` } },
datalabels: {
anchor: 'end',
align: 'right',
color: '#000',
font: { weight: 'bold', size: 11 },
formatter: (value) => formatBytes(value),
display: true,
clip: false
}
},
onClick: (evt, elements) => {
if (elements.length > 0 &amp;&amp; currentBreakdownData.length > 0) {
const item = currentBreakdownData[elements[0].index];
const fullKey = item.fullTracebackKey || getFullTracebackKey(item.traceback);
showTraceback(item.traceback, Math.abs(item.diff || item.size), fullKey);
}
},
scales: {
x: { beginAtZero: false, ticks: { callback: formatBytes }, title: { display: true, text: 'Memory Usage / Difference' } },
y: { title: { display: true, text: 'File:Line' }, ticks: { maxRotation: 0, font: { size: 11 } } }
}
},
plugins: [ChartDataLabels]
});
// Initial render
updateBarChart();
</script>
</body>
</html>
</template>
</odoo>

View file

@ -11,7 +11,7 @@
display: block;
font-size: 16px;
{{ neutralize_banner_style or '' }}">
<t t-out="neutralize_banner_text">This database is neutralized.</t>
<t t-out="neutralize_banner_text">Database neutralized for testing: no emails sent, etc.</t>
</span>
</div>
</xpath>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.actions.server" id="download_contact">
<field name="name">Download (vCard)</field>
<field name="model_id" ref="model_res_partner"/>
<field name="binding_model_id" ref="model_res_partner"/>
<field name="binding_view_types">form,list,kanban</field>
<field name="state">code</field>
<field name="code">
action = {
'type': 'ir.actions.act_url',
'url': '/web/partner/vcard?partner_ids=' + ','.join(map(str, records.ids)),
'target': 'download',
}
</field>
</record>
</odoo>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="config_speedscope_index">
<link rel="stylesheet" href="/web/assets/any/web.assets_web.min.css" />
<script src="/web/assets/any/web.assets_web.min.js" type="text/javascript"/>
<div class="container-md p-0">
<div class="o_form_view align-items-center">
<div class="row">
<div class="col" t-if="profiles._compute_has_memory()">
<div class="o_form_sheet">
<form method="get" t-attf-action="/web/profile_config/{{profile_str}}">
<h4>Memory profile</h4>
<div class="mt-3">
<label for="memory_limit">Memory limit (in bytes)</label>
<input type="number" id="memory_limit" name="memory_limit" class="form-control" value="1000"/>
</div>
<input type="hidden" name="profile_id" t-att-value="profile_str" />
<div class="mt-3">
<button type="submit" class="btn btn-primary" name="action" value="memory_open">open</button>
</div>
</form>
</div>
</div>
<div class="col">
<div class="o_form_sheet">
<!-- Single Form -->
<form method="get" t-attf-action="/web/speedscope/{{profile_str}}">
<h4>Speedscope</h4>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="constant_time" name="constant_time" value="True" />
<label class="form-check-label" for="constant_time">Constant Time</label>
</div>
<!-- add more variable here -->
<div class="form-check">
<input type="checkbox" class="form-check-input" id="aggregate_sql" name="aggregate_sql" value="True" />
<label class="form-check-label" for="aggregate_sql">Aggregate SQL</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="use_execution_context" name="use_execution_context" value="True" checked="1"/>
<label class="form-check-label" for="use_execution_context">Use execution context</label>
</div>
<div t-if="len(profiles) > 1">
<hr/>
<h4>Multiple profile</h4>
<label class="form-check-label" for="profile_aggregation_mode">Aggregation mode</label>
<select class="form-select" name="profile_aggregation_mode" id="profile_aggregation_mode">
<option value="tabs">Separated (one per tab)</option>
<option value="temporal">Temporal (experimental)</option>
</select>
<span id="temporal_warning" class="alert alert-warning" style="display:none"><b>Warning:</b> Temporal mode will merge all samples.<br/>It can lead to partially invalid result in case of concurrent profiles.<br/>Use with caution </span>
<script>
document.getElementById('profile_aggregation_mode').addEventListener('change', function (event) {
document.getElementById('temporal_warning').style.display = event.target.value === 'temporal'? 'block': 'none';
});
</script>
</div>
<hr/>
<h4>Display presets</h4>
<t t-set="has_sql" t-value="any(profile.sql for profile in profiles)"/>
<t t-set="has_traces" t-value="any(profile.traces_async for profile in profiles)"/>
<div class="form-check" t-if="has_sql and has_traces">
<input type="checkbox" class="form-check-input" id="combined_profile" name="combined_profile" value="True" t-att-checked="default_params.get('combined_profile')"/>
<label class="form-check-label" for="combined_profile">Frames + sql (Combined)</label>
</div>
<div class="form-check" t-if="has_sql">
<input type="checkbox" class="form-check-input" id="sql_no_gap_profile" name="sql_no_gap_profile" value="True" t-att-checked="default_params.get('sql_no_gap_profile')"/>
<label class="form-check-label" for="sql_no_gap_profile">SQL no gap</label>
</div>
<div class="form-check" t-if="has_sql">
<input type="checkbox" class="form-check-input" id="sql_density_profile" name="sql_density_profile" value="True" t-att-checked="default_params.get('sql_density_profile')"/>
<label class="form-check-label" for="sql_density_profile">SQL density</label>
</div>
<div class="form-check" t-if="has_traces">
<input type="checkbox" class="form-check-input" id="frames_profile" name="frames_profile" value="True" t-att-checked="default_params.get('frames_profile')"/>
<label class="form-check-label" for="frames_profile">Frames</label>
</div>
<input type="hidden" name="profile_id" t-att-value="profile_str" />
<div class="mt-3">
<button type="submit" class="btn btn-secondary" name="action" value="speedscope_download_json"><i class="fa fa-download"/> (json)</button>
<button type="submit" class="btn btn-secondary" name="action" value="speedscope_download_html"><i class="fa fa-download"/> (html)</button>
<button type="submit" class="btn btn-primary" name="action" value="speedscope_open">Open</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
</odoo>

View file

@ -6,7 +6,7 @@
<head>
<meta charset="UTF-8"/>
<title>Speedscope for odoo</title>
<script t-if="profile">window.location.hash="#profileURL=<t t-esc="url_root"/>web/content/ir.profile/<t t-esc="profile.id"/>/speedscope"</script>
<script>window.location.hash="#localProfilePath=1"</script>
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet"/>
<link rel="stylesheet"
t-attf-href="{{cdn}}reset.8c46b7a1.css"
@ -20,6 +20,12 @@
crossorigin="anonymous"
integrity="sha256-CvDqAOMjq0Sv/D59O5JSbzzXoClZ3rptt6ts8D/6CWw="
></script>
<script>
const b64 = '<t t-esc="speedscope_base64"/>';
document.addEventListener("DOMContentLoaded", () => {
speedscope.loadFileFromBase64('Profile', b64);
});
</script>
</body>
</html>
</template>

View file

@ -3,19 +3,27 @@
<!-- Call this template instead of "web.assets_tests" to have the proper conditional check -->
<template id="conditional_assets_tests" name="Tests Assets Bundle">
<t t-call-assets="web.assets_tests" t-if="'tests' in debug or test_mode_enabled" defer_load="True" />
<t t-if="'tests' in debug or test_mode_enabled">
<t t-if="ignore_missing_deps">
<!-- FIXME: This is only to ignore the errors for the lazy loading. To allow all tests assets and tours to be in the same bundle, the assets_tests bundle ignores missing dependencies -->
<t t-call-assets="web.__assets_tests_call__" defer_load="True" />
</t>
<t t-else="">
<t t-call-assets="web.assets_tests" defer_load="True" />
</t>
</t>
</template>
<template id="web.layout" name="Web layout">&lt;!DOCTYPE html&gt;
<html t-att="html_data or {}">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title t-esc="title or 'Odoo'"/>
<link type="image/x-icon" rel="shortcut icon" t-att-href="x_icon or '/web/static/img/favicon.ico'"/>
<script id="web.layout.odooscript" type="text/javascript">
var odoo = {
csrf_token: "<t t-nocache="The csrf token must always be up to date." t-esc="request.csrf_token(None)"/>",
csrf_token: "<t t-esc="request.csrf_token(None)"/>",
debug: "<t t-esc="debug"/>",
};
</script>
@ -36,7 +44,7 @@
<t t-call-assets="web.assets_frontend" t-js="false"/>
</xpath>
<xpath expr="//head/script[@id='web.layout.odooscript']" position="after">
<script t-nocache="Session information should always be up to date." type="text/javascript">
<script type="text/javascript">
odoo.__session_info__ = <t t-out="json.dumps(request.env['ir.http'].get_frontend_session_info())"/>;
if (!/(^|;\s)tz=/.test(document.cookie)) {
const userTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;
@ -44,7 +52,7 @@
}
</script>
<t t-call-assets="web.assets_frontend_minimal" t-css="false" defer_load="True"/>
<t t-call="web.conditional_assets_tests"/>
<t t-call="web.conditional_assets_tests" ignore_missing_deps="True"/>
<t t-call-assets="web.assets_frontend_lazy" t-css="false" lazy_load="True"/>
</xpath>
<xpath expr="//t[@t-out='0']" position="replace">
@ -57,11 +65,11 @@
<main>
<t t-out="0"/>
</main>
<footer t-cache="no_footer,no_copyright" t-if="not no_footer" id="bottom" data-anchor="true" t-attf-class="bg-light o_footer">
<footer t-if="not no_footer" id="bottom" data-anchor="true" t-attf-class="bg-light o_footer">
<div id="footer"/>
<div t-if="not no_copyright" class="o_footer_copyright">
<div class="container py-3">
<div class="row">
<div class="row row-gap-2">
<div class="col-sm text-center text-sm-start text-muted">
<span class="o_footer_copyright_name me-2">Copyright &amp;copy; <span t-field="res_company.name" itemprop="name">Company name</span></span>
</div>
@ -109,7 +117,7 @@
<div class="container py-5">
<div t-attf-class="card border-0 mx-auto bg-100 {{login_card_classes}} o_database_list" style="max-width: 300px;">
<div class="card-body">
<div t-attf-class="text-center pb-3 border-bottom {{'mb-3' if form_small else 'mb-4'}}">
<div class="text-center pb-3 border-bottom mb-4">
<img t-attf-src="/web/binary/company_logo{{ '?dbname='+db if db else '' }}" alt="Logo" style="max-height:120px; max-width: 100%; width:auto"/>
</div>
<t t-out="0"/>
@ -127,25 +135,32 @@
<template id="web.login" name="Login">
<t t-call="web.login_layout">
<form class="oe_login_form" role="form" t-attf-action="/web/login" method="post" onsubmit="this.action = '/web/login' + location.hash">
<div class="oe_structure" id="oe_structure_login_top"/>
<owl-component t-if="not login" name="web.user_switch" />
<form t-attf-class="oe_login_form #{'' if login else 'd-none'}" role="form" t-attf-action="/web/login" method="post" onsubmit="this.action = '/web/login' + location.hash" data-captcha="login">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="mb-3" t-if="databases and len(databases) &gt; 1">
<label for="db" class="col-form-label">Database</label>
<div t-attf-class="input-group {{'input-group-sm' if form_small else ''}}">
<input type="text" name="db" t-att-value="request.db" id="db" t-attf-class="form-control #{'form-control-sm' if form_small else ''}" required="required" readonly="readonly"/>
<a role="button" href="/web/database/selector" class="btn btn-secondary">Select <i class="fa fa-database" role="img" aria-label="Database" title="Database"></i></a>
<div class="input-group">
<input type="text" name="db" t-att-value="request.db" id="db" class="form-control" required="required" readonly="readonly"/>
<a role="button" href="/web/database/selector" class="btn border border-start-0">Select <i class="fa fa-database" role="img" aria-label="Database" title="Database"></i></a>
</div>
</div>
<div class="mb-3 field-login">
<label for="login" class="form-label">Email</label>
<input type="text" placeholder="Email" name="login" t-att-value="login" id="login" t-attf-class="form-control #{'form-control-sm' if form_small else ''}" required="required" autocomplete="username" autofocus="autofocus" autocapitalize="off"/>
<label for="login" class="form-label d-flex justify-content-between">Email</label>
<input type="text" placeholder="Enter your email" name="login" t-att-value="login" id="login" class="form-control" required="required" autocapitalize="off" autocomplete="username"/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" placeholder="Password" name="password" id="password" t-attf-class="form-control #{'form-control-sm' if form_small else ''}" required="required" autocomplete="current-password" t-att-autofocus="'autofocus' if login else None" maxlength="4096"/>
<div class="o_caps_lock_warning">
<label for="password" class="form-label d-flex justify-content-between">Password</label>
<div class="input-group mb-1">
<input type="password" placeholder="Enter your password" name="password" id="password" class="form-control" required="required" autocomplete="current-password" t-att-autofocus="'autofocus' if login else None" maxlength="4096"/>
<button type="button" class="btn btn-sm border border-start-0 o_show_password">
<i class="fa fa-eye"/>
</button>
</div>
</div>
<p class="alert alert-danger" t-if="error" role="alert">
@ -155,16 +170,18 @@
<t t-esc="message"/>
</p>
<div t-attf-class="clearfix oe_login_buttons text-center gap-1 d-grid mb-1 {{'pt-2' if form_small else 'pt-3'}}">
<div class="oe_login_buttons text-center d-grid mb-1 pt-3">
<button type="submit" class="btn btn-primary">Log in</button>
<t t-if="debug">
<button type="submit" name="redirect" value="/web/become" class="btn btn-link btn-sm">Log in as superuser</button>
</t>
<div class="o_login_auth"/>
<t t-call="web.login_oauth"/>
</div>
<input type="hidden" name="type" value="password"/>
<input type="hidden" name="redirect" t-att-value="redirect"/>
</form>
<div class="oe_structure" id="oe_structure_login_bottom"/>
</t>
</template>
@ -181,13 +198,28 @@
</t>
</template>
<template id="web.login_oauth" name="Login OAuth">
<!-- Use auth_btns to insert login options displayed under the
login form -->
<t t-set="auth_btns" t-value="[]"/>
<div t-if="auth_btns" class="o_login_auth">
<em class="d-block my-3 small text-center text-muted">- or -</em>
<div class="list-group">
<a t-foreach="auth_btns" t-as="p" t-attf-class="{{p.get('list_item_class')}} list-group-item list-group-item-action d-flex align-items-center gap-2 py-2" t-att-href="p.get('auth_link') or '#'">
<i t-att-class="p['css_class']" role="presentation"/>
<span class="mx-auto" t-out="p['body']"/>
</a>
</div>
</div>
</template>
<template id="web.test_helpers">
<t t-call-assets="web.tests_assets" t-js="False"/>
<style>
body {
position: relative; /* bootstrap-datepicker needs this */
}
body:not(.debug) .modal-backdrop, body:not(.debug) .modal, body:not(.debug) .ui-autocomplete {
body:not(.debug) .modal-backdrop, body:not(.debug) .modal:not(.o_module_error), body:not(.debug) .ui-autocomplete {
opacity: 0 !important;
}
#qunit-testrunner-toolbar label {
@ -202,15 +234,31 @@
<t t-call-assets="web.tests_assets" t-css="False"/>
</template>
<template id="web.unit_tests_suite">
<t t-call="web.layout">
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
<t t-set="title">Web Unit Tests</t>
<t t-set="head">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<script type="text/javascript">
odoo.__session_info__ = <t t-out="json.dumps(session_info)"/>;
</script>
<t t-call-assets="web.assets_unit_tests_setup" />
<t t-call-assets="web.assets_unit_tests" defer_load="True" />
</t>
</t>
</template>
<template id="web.qunit_suite">
<t t-call="web.layout">
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
<t t-set="title">Web Tests</t>
<t t-set="head">
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_backend" t-js="false"/>
<t t-call-assets="web.tests_assets_common" t-css="false"/>
<t t-call-assets="web.assets_backend" t-css="false"/>
<script type="text/javascript">
odoo.__session_info__ = <t t-out="json.dumps(session_info)"/>;
</script>
<t t-call="web.test_helpers"/>
@ -222,118 +270,117 @@
</t>
</template>
<template id="web.qunit_mobile_suite">
<t t-call="web.layout">
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
<t t-set="title">Web Mobile Tests</t>
<t t-set="head">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_backend" t-js="false"/>
<t t-call-assets="web.tests_assets_common" t-css="false"/>
<t t-call-assets="web.assets_backend" t-css="false"/>
<t t-call="web.test_helpers"/>
<t t-call-assets="web.qunit_mobile_suite_tests" t-js="false"/>
<t t-call-assets="web.qunit_mobile_suite_tests" t-css="false"/>
</t>
<div id="qunit"/>
<div id="qunit-fixture"/>
</t>
</template>
<template id="web.benchmark_suite">
<t t-call="web.layout">
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
<t t-set="title">Web Benchmarks</t>
<t t-set="head">
<script type="text/javascript" src="/web/static/lib/benchmarkjs/lodash.js"></script>
<script type="text/javascript" src="/web/static/lib/benchmarkjs/benchmark.js"></script>
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_backend" t-js="false"/>
<t t-call-assets="web.assets_common" t-css="false"/>
<t t-call-assets="web.assets_backend" t-css="false"/>
<t t-call="web.test_helpers"/>
<script type="text/javascript">
QUnit.config.hidepassed = false;
</script>
<style>
body:not(.debug) .modal-backdrop, body:not(.debug) .modal, body:not(.debug) .ui-autocomplete {
opacity: 0 !important;
}
#qunit-testrunner-toolbar label {
font-weight: inherit;
margin-bottom: inherit;
}
#qunit-testrunner-toolbar input[type=text] {
width: inherit;
display: inherit;
}
</style>
<script type="text/javascript" src="/web/static/tests/views/list_benchmarks.js"></script>
<script type="text/javascript" src="/web/static/tests/views/kanban_benchmarks.js"></script>
<script type="text/javascript" src="/web/static/tests/views/form_benchmarks.js"></script>
</t>
<div id="qunit"/>
<div id="qunit-fixture"/>
</t>
</template>
<template id="web.webclient_bootstrap">
<t t-call="web.layout">
<t t-set="head_web">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="#875A7B"/>
<link rel="icon" sizes="192x192" href="/web/static/img/mobile-icons/android-192x192.png"/>
<!-- iOS Safari -->
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<link rel="apple-touch-icon" href="/web/static/img/mobile-icons/apple-152x152.png"/>
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#875A7B"/>
<meta name="msapplication-TileColor" content="#875A7B"/>
<meta name="msapplication-TileImage" content="/web/static/img/mobile-icons/windows-144x144.png"/>
<meta name="theme-color" content="#71639e"/>
<link rel="manifest" href="/web/manifest.webmanifest" crossorigin="use-credentials"/>
<link rel="apple-touch-icon" href="/web/static/img/odoo-icon-ios.png"/>
<script type="text/javascript">
odoo.__session_info__ = <t t-out="json.dumps(session_info)"/>;
odoo.reloadMenus = () => fetch(`/web/webclient/load_menus/${odoo.__session_info__.cache_hashes.load_menus}`).then(res => res.json());
odoo.loadMenusPromise = odoo.reloadMenus();
// Block to avoid leaking variables in the script scope
{
const { user_context, cache_hashes } = odoo.__session_info__;
// Prefetch translations to speedup webclient. This is done in JS because link rel="prefetch"
// is not yet supported on safari.
fetch(`/web/webclient/translations/${cache_hashes.translations}?lang=${user_context.lang}`);
odoo.__session_info__ = <t t-out="json.dumps(session_info)"/>;
const { user_context } = odoo.__session_info__;
const lang = new URLSearchParams(document.location.search).get("lang");
let menuURL = "/web/webclient/load_menus";
if (lang) {
user_context.lang = lang;
menuURL += `&amp;lang=${lang}`;
}
odoo.reloadMenus = () => fetch(menuURL, { cache: "no-store" }).then(res => res.json());
odoo.loadMenusPromise = odoo.reloadMenus();
}
</script>
<t t-if="request.httprequest.cookies.get('color_scheme') == 'dark'">
<t t-call-assets="web.dark_mode_assets_common" t-js="false"/>
<t t-call-assets="web.dark_mode_assets_backend" t-js="false"/>
<t t-call-assets="web.assets_web_print" media="print" t-js="false"/>
<t t-call-assets="web.assets_web" t-css="false"/>
<t t-if="color_scheme == 'dark'">
<t t-call-assets="web.assets_web_dark" media="screen" t-js="false"/>
</t>
<t t-else="">
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_backend" t-js="false"/>
<t t-call-assets="web.assets_web" media="screen" t-js="false"/>
</t>
<t t-call-assets="web.assets_common" t-css="false"/>
<t t-call-assets="web.assets_backend" t-css="false"/>
<t t-call-assets="web.assets_backend_prod_only" t-css="false"/>
<t t-call="web.conditional_assets_tests"/>
<t t-call="web.conditional_assets_tests" media="screen"/>
</t>
<t t-set="head" t-value="head_web + (head or '')"/>
<t t-set="head" t-value="head_web + (head or '')" media="screen"/>
<t t-set="body_classname" t-value="'o_web_client'"/>
</t>
</template>
<template id="webclient_offline">
<t t-call="web.layout">
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
<t t-set="title">Offline</t>
<t t-set="head">
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", () => {
const cookies = document.cookie.split(';').map(c => c.trim());
if (cookies.includes('color_scheme=dark')) {
document.body.style.backgroundColor = "#262A36";
document.body.style.color = "#FFFFFF";
}
});
window.addEventListener('online', () => location.reload());
</script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
padding:0;
margin:0;
background-color: #fff;
color: rgb(17, 24, 39);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
height: 100vh;
width: 100vw;
user-select: none;
}
.card {
width: 80%;
}
.card img {
width: 96px;
height: auto;
filter: grayscale(.6);
}
.card button {
background-color: #714B67;
color: #FFFFFF;
border: 1px solid #714B67;
border-radius: .25rem;
padding: .5rem 1rem;
cursor: pointer;
font-size: 1.2rem;
font-weight: 500;
}
</style>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</t>
<div class="card">
<img t-attf-src="data:image/png;base64,{{odoo_icon}}" alt="Odoo logo"/>
<h1>You are offline</h1>
<p>Check your network connection and come back here. Odoo will load as soon as you're back online.</p>
<button onclick="location.reload()">Check again</button>
</div>
</t>
</template>
<template id="webclient_scoped_app">
<t t-call="web.layout">
<t t-set="html_data" t-value="{'style': 'height: 100%;'}"/>
<t t-set="title" t-value="'Install %s' % (app_name)"/>
<t t-set="x_icon" t-value="'/scoped_app_icon_png?app_id=%s' % (app_id)" />
<t t-set="head">
<t t-call-assets="web.assets_frontend_minimal"/>
<t t-call-assets="web.assets_frontend_lazy"/>
<link rel="apple-touch-icon" t-att-href="x_icon"/>
<link rel="manifest" t-att-href="safe_manifest_url" crossorigin="use-credentials" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</t>
<owl-component name="web.install_scoped_app" />
</t>
</template>
</odoo>