first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
from . import controllers
from . import models
+47
View File
@@ -0,0 +1,47 @@
{
'name': "custom_template_khmer",
'summary': "Short (1 phrase/line) summary of the module's purpose",
'description': """
Allows dynamic customization of:
- Font Family (Upload .ttf)
- Menu Header Background (Color or Image)
- Responsive design for Mobile/Desktop
- Works on Backend and Website
""",
'author': "My Company",
'license': 'LGPL-3',
'website': "https://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'thems and backend',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['web', 'website'],
# always loaded
'data': [
'security/ir.model.access.csv',
'views/res_config_settings_views.xml',
'templates/layout_inherit.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
'assets': {
'web.assets_backend': [
'custom_template_khmer/static/src/scss/custom_theme.css',
],
'website.assets_frontend': [
'custom_template_khmer/static/src/scss/custom_theme.css',
'custom_template_khmer/static/src/scss/fonts.css',
],
},
}
@@ -0,0 +1 @@
from . import main
+46
View File
@@ -0,0 +1,46 @@
# custom_template_khmer/controllers/main.py
from odoo import http
from odoo.http import request
import logging
_logger = logging.getLogger(__name__)
class CustomThemeController(http.Controller):
# @http.route('/custom_template_khmer/fonts', type='http', auth='public')
# def get_font(self):
# """Serve custom font file"""
# try:
# config = request.env['custom.theme.config'].sudo().search([], limit=1)
# if config and config.font_file:
# return request.make_response(
# config.font_file,
# headers=[
# ('Content-Type', 'font/ttf'),
# ('Content-Disposition', f'inline; filename={config.font_name}.ttf'),
# ('Cache-Control', 'public, max-age=31536000')
# ]
# )
# except Exception as e:
# _logger.error(f"Error serving font: {str(e)}")
#
# return request.make_response("", headers=[('Content-Type', 'text/plain')])
@http.route('/custom_template_khmer/menu_image', type='http', auth='public')
def get_menu_image(self):
"""Serve menu background image"""
try:
config = request.env['custom.theme.config'].sudo().search([], limit=1)
if config and config.menu_bg_image:
return request.make_response(
config.menu_bg_image,
headers=[
('Content-Type', 'image/png'),
('Cache-Control', 'public, max-age=31536000')
]
)
except Exception as e:
_logger.error(f"Error serving menu image: {str(e)}")
return request.make_response("", headers=[('Content-Type', 'text/plain')])
+30
View File
@@ -0,0 +1,30 @@
<odoo>
<data>
<!--
<record id="object0" model="custom_template_khmer.custom_template_khmer">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="custom_template_khmer.custom_template_khmer">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="custom_template_khmer.custom_template_khmer">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="custom_template_khmer.custom_template_khmer">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="custom_template_khmer.custom_template_khmer">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>
+2
View File
@@ -0,0 +1,2 @@
from . import custom_theme_config
from . import res_config_settings
@@ -0,0 +1,118 @@
# custom_template_khmer/models/custom_theme_config.py
from odoo import models, fields, api
import base64
import io
import logging
_logger = logging.getLogger(__name__)
try:
from fontTools.ttLib import TTFont
HAS_FONTTOOLS = True
except ImportError:
HAS_FONTTOOLS = False
_logger.warning("fontTools library not installed. Auto-font-name detection will not work.")
class CustomThemeConfig(models.Model):
_name = 'custom.theme.config'
_description = 'Custom Theme Configuration (Singleton)'
name = fields.Char(default='Main Configuration')
# Binary Fields
font_file = fields.Binary(string="Custom Font File (.ttf)", attachment=True)
menu_bg_image = fields.Binary(string="Menu Background Image", attachment=True)
# Simple Fields
font_name = fields.Char(string="Font Family Name")
menu_bg_color = fields.Char(string="Menu Background Color", default="#714B67")
is_responsive = fields.Boolean(string="Enable Mobile Responsiveness", default=True)
@api.onchange('font_file')
def _onchange_font_file(self):
"""Automatically extract font name when file is uploaded"""
if self.font_file and HAS_FONTTOOLS:
try:
# Decode the base64 file
font_data = base64.b64decode(self.font_file)
font_io = io.BytesIO(font_data)
# Open font with fontTools
font = TTFont(font_io)
# Get the 'name' table
name_table = font['name']
# Try to find the PostScript Name (ID 6) or Full Name (ID 4)
# Priority: PostScript Name > Full Name > Font Family
font_name = None
for record in name_table.names:
# nameID 6 = PostScript Name (Best for CSS)
if record.nameID == 6:
font_name = str(record)
break
# nameID 4 = Full Name
elif record.nameID == 4 and not font_name:
font_name = str(record)
# Fallback to Family Name (ID 1) if others missing
if not font_name:
for record in name_table.names:
if record.nameID == 1:
font_name = str(record)
break
if font_name:
self.font_name = font_name
return {
'notification': {
'type': 'success',
'message': f'Font detected: {font_name}',
'sticky': False,
}
}
else:
return {
'notification': {
'type': 'warning',
'message': 'Could not detect font name. Please enter manually.',
'sticky': True,
}
}
except Exception as e:
_logger.error(f"Error extracting font name: {e}")
return {
'notification': {
'type': 'danger',
'message': f'Error reading font file: {str(e)}',
'sticky': True,
}
}
elif not HAS_FONTTOOLS:
return {
'notification': {
'type': 'warning',
'message': 'Install "fonttools" library to auto-detect font names.',
'sticky': True,
}
}
@api.model
def get_config(self):
return self.search([], limit=1)
# ✅ ADD THIS FIELD to store the original filename
font_file_name = fields.Char(string="Original Filename", compute='_compute_font_file_name', store=True)
font_file = fields.Binary(string="Custom Font File (.ttf)", attachment=True)
@api.depends('font_file')
def _compute_font_file_name(self):
for record in self:
if record.font_file:
record.font_file_name = "font_uploaded.ttf"
else:
record.font_file_name = False
@@ -0,0 +1,50 @@
from odoo import models, fields, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
# ✅ CRITICAL FIX: Use Many2one to link to the singleton
# This is the ONLY field the settings wizard needs to save explicitly
khmer_theme_config_id = fields.Many2one(
'custom.theme.config',
string='Theme Configuration',
required=True,
default=lambda self: self._get_default_theme_config()
)
# ✅ Related Fields (Read/Write via the Many2one)
# These will automatically update the linked record when the Many2one is saved
khmer_font_file = fields.Binary(
string="Custom Font File (.ttf)",
related='khmer_theme_config_id.font_file',
readonly=False
)
khmer_font_name = fields.Char(
string="Font Family Name",
related='khmer_theme_config_id.font_name',
readonly=False
)
khmer_menu_bg_color = fields.Char(
string="Menu Background Color",
related='khmer_theme_config_id.menu_bg_color',
readonly=False
)
khmer_menu_bg_image = fields.Binary(
string="Menu Background Image",
related='khmer_theme_config_id.menu_bg_image',
readonly=False
)
khmer_is_responsive = fields.Boolean(
string="Enable Mobile Responsiveness",
related='khmer_theme_config_id.is_responsive',
readonly=False
)
khmer_font_file_name = fields.Char(string="Font Filename", related='khmer_theme_config_id.font_file_name',
readonly=False)
@api.model
def _get_default_theme_config(self):
config = self.env['custom.theme.config'].search([], limit=1)
if not config:
config = self.env['custom.theme.config'].create({'name': 'Main Configuration'})
return config.id
@@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_custom_theme_config_user,custom.theme.config.user,model_custom_theme_config,base.group_user,1,1,1,0
access_custom_theme_config_admin,custom.theme.config.admin,model_custom_theme_config,base.group_system,1,1,1,1
access_res_config_settings_admin,res.config.settings.admin,model_res_config_settings,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_custom_theme_config_user custom.theme.config.user model_custom_theme_config base.group_user 1 1 1 0
3 access_custom_theme_config_admin custom.theme.config.admin model_custom_theme_config base.group_system 1 1 1 1
4 access_res_config_settings_admin res.config.settings.admin model_res_config_settings base.group_system 1 1 1 1
@@ -0,0 +1,30 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useEffect } from "@odoo/owl";
export function themeLoaderPlugin(env, node) {
useEffect(() => {
// Apply theme after render
const applyTheme = async () => {
try {
const response = await fetch('/custom_template_khmer/get_config');
const config = await response.json();
if (config.menu_bg_color) {
document.documentElement.style.setProperty(
'--khmer-menu-bg-color',
config.menu_bg_color
);
}
} catch (error) {
console.warn('Theme config load failed:', error);
}
};
applyTheme();
}, []);
}
// Register as a service or patch existing components
// Note: For simple CSS themes, JS is usually NOT needed
@@ -0,0 +1,631 @@
body,
.o_form_view,
.o_form_view .o_field_widget,
.o_form_view label,
.o_form_view .o_wrap_label,
.o_form_view h1, .o_form_view h2, .o_form_view h3,
.o_list_renderer th,
.o_list_renderer td,
.o_main_navbar .o_menu_brand,
.o_main_navbar .o_menu_sections a,
.breadcrumb,
.o_statusbar_status button,
select,
select option,
.o_field_selection select,
.o_field_selection select option,
.o_select_menu,
.o_select_menu .o_select_menu_item,
.o_select_menu_item_label,
.dropdown-menu,
.dropdown-menu .dropdown-item,
.o_field_many2one_selection .o_external_button,
.ui-autocomplete,
.ui-autocomplete .ui-menu-item {
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
.o_main_navbar,
.o_main_navbar .o_menu_sections,
.o_main_navbar .o_menu_sections > *,
.o_main_navbar .o_menu_brand,
.o_navbar,
.o_menu_sections {
background-color: #0a5e98 !important;
background: #0a5e98 !important;
}
/* Section header - orange/brown like design */
.o_xf_section_header {
color: #c0392b;
font-weight: bold;
font-size: 1.1rem;
margin: 16px 0 8px 0;
}
/* Approvers group title font size */
.o_group.o_inner_group .o_horizontal_separator,
.o_horizontal_separator {
font-size: 1.2rem !important;
font-weight: bold !important;
}
/* Make source container full width matching the table */
.o_xf_source_container {
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
.o_xf_source_label {
white-space: nowrap !important;
flex-shrink: 0 !important;
min-width: 180px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.o_xf_source_container {
align-items: stretch !important;
}
.o_xf_source_container > div {
display: flex !important;
align-items: center !important;
}
/* Inline radio widget using field name selector */
[name="document_source"] {
display: flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
align-items: center !important;
}
[name="document_source"] .o_radio_item {
display: inline-flex !important;
align-items: center !important;
white-space: nowrap !important;
flex-shrink: 0 !important;
margin: 0 !important;
}
[name="document_source"] .o_radio_item:not(:last-child)::after {
content: '|' !important;
margin: 0 10px !important;
color: #999 !important;
}
[name="document_source"] input[type="radio"] {
display: none !important;
}
[name="document_source"] label {
cursor: pointer !important;
color: #0a5e98 !important;
margin: 0 !important;
padding: 0 !important;
font-weight: normal !important;
background: none !important;
border: none !important;
white-space: nowrap !important;
}
[name="document_source"] .o_radio_item.o_checked label {
font-weight: bold !important;
color: #000 !important;
}
/* ── Confirmation dialog — Khmer ── */
/* Title: replace "Confirmation" */
.o_dialog .modal-title {
font-size: 0 !important;
color: transparent !important;
}
.o_dialog .modal-title::after {
content: 'ការបញ្ជាក់' !important;
font-size: 1.1rem !important;
color: #000 !important;
font-weight: 600 !important;
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
/* OK button: target buttons with no [name] attr (confirmation dialog only) */
.o_dialog .modal-footer button.btn-primary:not([name]) {
font-size: 0 !important;
color: transparent !important;
}
.o_dialog .modal-footer button.btn-primary:not([name])::after {
content: 'យល់ព្រម' !important;
font-size: 0.875rem !important;
color: #fff !important;
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
/* Cancel button: no [name], no [special] = confirmation dialog Cancel */
.o_dialog .modal-footer button.btn-secondary:not([name]):not([special]) {
font-size: 0 !important;
color: transparent !important;
}
.o_dialog .modal-footer button.btn-secondary:not([name]):not([special])::after {
content: 'បោះបង់' !important;
font-size: 0.875rem !important;
color: #212529 !important;
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
/* Wizard Cancel button (special="cancel") */
.o_dialog .modal-footer button[special="cancel"] {
font-size: 0 !important;
color: transparent !important;
}
.o_dialog .modal-footer button[special="cancel"]::after {
content: 'បោះបង់' !important;
font-size: 0.875rem !important;
color: #212529 !important;
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
/* ── Chatter buttons — Khmer ── */
.o_chatter_button_new_message {
font-size: 0 !important;
color: transparent !important;
}
.o_chatter_button_new_message::after {
content: 'ផ្ញើសារ' !important;
font-size: 0.875rem !important;
color: #fff !important;
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
.o_chatter_button_log_note {
font-size: 0 !important;
color: transparent !important;
}
.o_chatter_button_log_note::after {
content: 'កត់សម្គាល់' !important;
font-size: 0.875rem !important;
color: inherit !important;
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
/* Replace "Add a line" with Khmer in approver table */
.o_field_one2many[name="approver_ids"] a[role="button"] {
font-size: 0 !important;
color: transparent !important;
}
.o_field_one2many[name="approver_ids"] a[role="button"]::after {
content: 'បន្ថែម' !important;
font-size: 0.875rem !important;
color: #0a5e98 !important;
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
/* Remove background from sort icon in approver table header */
.o_field_one2many[name="approver_ids"] .o_list_sortable_icon {
background: none !important;
background-color: transparent !important;
color: #ffffff !important;
}
/* Approver table - header */
.o_form_view .o_field_one2many .o_list_renderer thead th {
background-color: #2e75b6 !important;
color: #ffffff !important;
text-align: center !important;
padding: 8px 10px !important;
font-weight: bold !important;
border: 1px solid #1a5a96 !important;
white-space: nowrap;
}
/* Approver table - rows */
.o_form_view .o_field_one2many .o_list_renderer tbody td {
padding: 6px 10px !important;
border: 1px solid #d0d0d0 !important;
vertical-align: middle !important;
text-align: center !important;
}
/* Approver table - alternating rows */
.o_form_view .o_field_one2many .o_list_renderer tbody tr:nth-child(odd) td {
background-color: #ffffff !important;
}
.o_form_view .o_field_one2many .o_list_renderer tbody tr:nth-child(even) td {
background-color: #e8f0f9 !important;
}
/* Hover row highlight */
.o_form_view .o_field_one2many .o_list_renderer tbody tr:hover td {
background-color: #cce0f5 !important;
}
/* Remove default Odoo row borders */
.o_form_view .o_field_one2many .o_list_renderer table {
border-collapse: collapse !important;
width: 100% !important;
}
/* Hide selection checkbox in approver_ids list */
.o_field_one2many[name="approver_ids"] .o_list_record_selector,
.o_field_one2many[name="approver_ids"] .o_list_record_selector * {
display: none !important;
width: 0 !important;
min-width: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
}
.o_field_one2many[name="approver_ids"] input[type="checkbox"] {
display: none !important;
}
.btn-primary,
.o_form_button_save,
button.oe_highlight,
.o_statusbar_buttons .oe_highlight {
background-color: #0a5e98 !important;
border-color: #0a5e98 !important;
color: #fff !important;
}
/* ===================================================
Section ខ — Modern Card Design
=================================================== */
/* Section title */
.o_xf_section_b .o_horizontal_separator {
font-size: 1.05rem !important;
font-weight: 700 !important;
color: #0a5e98 !important;
padding: 6px 0 10px 2px !important;
border-bottom: 2px solid #0a5e98 !important;
margin-bottom: 12px !important;
}
/* Card wrapper */
.o_xf_table {
width: 100% !important;
background: #ffffff !important;
border-radius: 10px !important;
box-shadow: 0 2px 8px rgba(10, 94, 152, 0.10),
0 0 0 1px #dde8f5 !important;
overflow: hidden !important;
}
/* ── Row ── */
.o_xf_row {
display: flex !important;
min-height: 50px !important;
border-bottom: 1px solid #eef3fa !important;
}
.o_xf_row:last-child { border-bottom: none !important; }
.o_xf_row_tall {
align-items: stretch !important;
min-height: 80px !important;
}
/* ── Label cell ── */
.o_xf_cell_label {
flex: 0 0 200px !important;
width: 200px !important;
background: #f4f7fb !important;
border-right: 3px solid #0a5e98 !important;
padding: 0 16px !important;
display: flex !important;
align-items: center !important;
font-size: 0.875rem !important;
font-weight: 600 !important;
color: #234d73 !important;
white-space: nowrap !important;
}
/* ── Value cell ── */
.o_xf_cell_value {
flex: 1 !important;
padding: 0 16px !important;
display: flex !important;
align-items: center !important;
background: #ffffff !important;
min-width: 0 !important;
}
.o_xf_cell_value > .o_field_widget {
width: 100% !important;
min-width: 0 !important;
}
/* Input fields inside value cell — clean underline style */
.o_xf_cell_value .o_field_widget input.o_input,
.o_xf_cell_value .o_field_widget textarea.o_input {
border: none !important;
border-bottom: 1.5px solid #d0dcea !important;
border-radius: 0 !important;
background: transparent !important;
padding: 4px 2px !important;
font-size: 0.9rem !important;
color: #1a3550 !important;
width: 100% !important;
transition: border-color 0.15s !important;
}
.o_xf_cell_value .o_field_widget input.o_input:focus,
.o_xf_cell_value .o_field_widget textarea.o_input:focus {
border-bottom-color: #0a5e98 !important;
outline: none !important;
box-shadow: none !important;
}
/* Many2one field styling */
.o_xf_cell_value .o_field_many2one .o_input_dropdown input {
border: none !important;
border-bottom: 1.5px solid #d0dcea !important;
border-radius: 0 !important;
background: transparent !important;
font-size: 0.9rem !important;
color: #1a3550 !important;
}
.o_xf_cell_value .o_field_many2one .o_input_dropdown input:focus {
border-bottom-color: #0a5e98 !important;
box-shadow: none !important;
}
/* Selection (dropdown) field */
.o_xf_cell_value .o_field_selection select,
.o_xf_cell_value .o_field_widget select {
border: none !important;
border-bottom: 1.5px solid #d0dcea !important;
border-radius: 0 !important;
background: transparent !important;
font-size: 0.9rem !important;
color: #1a3550 !important;
padding: 4px 2px !important;
width: 100% !important;
}
/* Read-only values */
.o_xf_cell_value .o_field_widget.o_readonly,
.o_xf_cell_value .o_field_char.o_field_readonly,
.o_form_readonly .o_xf_cell_value .o_field_widget {
font-size: 0.9rem !important;
color: #1a3550 !important;
font-weight: 500 !important;
}
/* Placeholder text */
.o_xf_cell_value input::placeholder,
.o_xf_cell_value textarea::placeholder {
color: #b0bec5 !important;
font-style: italic !important;
font-size: 0.85rem !important;
}
/* ── Tall value cell (documents / description) ── */
.o_xf_cell_tall {
align-items: flex-start !important;
padding-top: 10px !important;
padding-bottom: 10px !important;
}
/* ── Split cell: file | note ── */
.o_xf_cell_split {
padding: 0 !important;
align-items: stretch !important;
}
.o_xf_split_left,
.o_xf_split_right {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
padding: 8px 16px !important;
gap: 5px !important;
}
.o_xf_split_divider {
width: 1px !important;
background: #dde8f5 !important;
flex-shrink: 0 !important;
}
.o_xf_split_hint {
font-size: 0.7rem !important;
color: #90aac4 !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 0.07em !important;
}
.o_xf_split_left .o_field_widget,
.o_xf_split_right .o_field_widget {
width: 100% !important;
}
/* ── Upload zone (ឯកសារ row) ── */
.o_xf_upload_zone {
flex-direction: column !important;
align-items: flex-start !important;
gap: 8px !important;
padding: 12px 16px !important;
}
.o_xf_upload_zone > .o_field_widget {
width: 100% !important;
}
/* Wrapper for the whole many2many_binary widget */
.o_xf_upload_zone .o_field_many2many_binary {
display: flex !important;
flex-direction: column !important;
gap: 6px !important;
width: 100% !important;
}
/* ── Style the raw file input ── */
.o_xf_upload_zone input[type="file"] {
width: 100% !important;
font-size: 0.84rem !important;
color: #555 !important;
cursor: pointer !important;
border: 1.5px dashed #a0bfdf !important;
border-radius: 8px !important;
padding: 8px 12px !important;
background: #f7fafd !important;
transition: border-color 0.15s, background 0.15s !important;
}
.o_xf_upload_zone input[type="file"]:hover {
border-color: #0a5e98 !important;
background: #edf4ff !important;
}
/* "Choose Files" browser button part */
.o_xf_upload_zone input[type="file"]::file-selector-button {
padding: 5px 16px !important;
border: none !important;
border-radius: 5px !important;
background: #0a5e98 !important;
color: #ffffff !important;
font-size: 0.83rem !important;
font-weight: 600 !important;
cursor: pointer !important;
margin-right: 12px !important;
transition: background 0.15s !important;
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif !important;
}
.o_xf_upload_zone input[type="file"]::file-selector-button:hover {
background: #084e82 !important;
}
/* ── Uploaded file rows ── */
.o_xf_upload_zone .o_field_many2many_binary .o_attachments,
.o_xf_upload_zone .o_field_many2many_binary ul {
width: 100% !important;
display: flex !important;
flex-direction: column !important;
gap: 5px !important;
list-style: none !important;
padding: 0 !important;
margin: 0 !important;
}
.o_xf_upload_zone .o_field_many2many_binary .o_attachment,
.o_xf_upload_zone .o_field_many2many_binary li {
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 6px 12px !important;
background: #edf4ff !important;
border: 1px solid #c5daef !important;
border-radius: 6px !important;
font-size: 0.84rem !important;
color: #1a3d5c !important;
}
.o_xf_upload_zone .o_field_many2many_binary .o_attachment a,
.o_xf_upload_zone .o_field_many2many_binary li a {
color: #0a5e98 !important;
text-decoration: none !important;
font-weight: 500 !important;
flex: 1 !important;
}
.o_xf_upload_zone .o_field_many2many_binary .o_attachment a:hover,
.o_xf_upload_zone .o_field_many2many_binary li a:hover {
text-decoration: underline !important;
}
/* Delete button */
.o_xf_upload_zone .o_field_many2many_binary .o_delete,
.o_xf_upload_zone .o_field_many2many_binary .delete {
color: #c0392b !important;
opacity: 0.55 !important;
cursor: pointer !important;
font-size: 0.9rem !important;
margin-left: auto !important;
}
.o_xf_upload_zone .o_field_many2many_binary .o_delete:hover {
opacity: 1 !important;
}
/* ── document_ids list in non-draft (ឯកសារ row) ── */
.o_xf_doc_list .o_list_renderer {
border: none !important;
box-shadow: none !important;
}
.o_xf_doc_list .o_list_renderer thead { display: none !important; }
.o_xf_doc_list .o_list_renderer tbody td {
border: none !important;
padding: 3px 6px !important;
background: transparent !important;
}
.o_xf_doc_list .o_list_renderer tbody tr {
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 4px 8px !important;
background: #edf4ff !important;
border: 1px solid #c5daef !important;
border-radius: 6px !important;
margin-bottom: 4px !important;
}
/* ── File download list (non-draft state) ── */
.o_xf_files_list {
display: flex !important;
flex-direction: column !important;
gap: 5px !important;
width: 100% !important;
padding: 4px 0 !important;
}
.o_xf_file_item {
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 6px 12px !important;
background: #edf4ff !important;
border: 1px solid #c5daef !important;
border-radius: 6px !important;
font-size: 0.84rem !important;
}
.o_xf_file_item a {
color: #0a5e98 !important;
text-decoration: none !important;
font-weight: 500 !important;
flex: 1 !important;
}
.o_xf_file_item a:hover { text-decoration: underline !important; }
.o_xf_file_icon { color: #5a9fd4 !important; font-size: 0.85rem !important; }
/* ── Readonly many2many_binary: ensure files are always visible ── */
.o_xf_upload_zone .o_field_many2many_binary {
display: flex !important;
flex-direction: column !important;
gap: 6px !important;
width: 100% !important;
}
/* Odoo 19 OWL: readonly renders files as <a> links directly */
.o_xf_upload_zone .o_field_many2many_binary a,
.o_xf_upload_zone .o_field_many2many_binary .o_form_uri,
.o_xf_upload_zone .o_field_many2many_binary span[title] {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
padding: 6px 12px !important;
background: #edf4ff !important;
border: 1px solid #c5daef !important;
border-radius: 6px !important;
font-size: 0.84rem !important;
color: #0a5e98 !important;
text-decoration: none !important;
font-weight: 500 !important;
}
.o_xf_upload_zone .o_field_many2many_binary a:hover {
text-decoration: underline !important;
background: #dceeff !important;
}
/* ── QR Code section ── */
.o_xf_qr_group {
margin-top: 16px !important;
padding: 24px 20px !important;
background: #f7fafd !important;
border-radius: 10px !important;
border: 1px dashed #b0cce8 !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
gap: 10px !important;
}
.o_xf_qr_label {
font-size: 0.78rem !important;
color: #7a9ec0 !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 0.09em !important;
}
.o_xf_qr_group .o_field_image img {
border-radius: 8px !important;
box-shadow: 0 2px 12px rgba(0,0,0,0.13) !important;
border: 4px solid #ffffff !important;
}
@@ -0,0 +1,38 @@
/* custom_template_khmer/static/src/css/custom_theme.css */
@font-face {
font-family: 'KhmerOS_content';
src:url('/custom_template_khmer/static/src/fonts/KhmerOS_content.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* Fallback variables */
:root {
--khmer-menu-bg: #714B67;
--khmer-font-family: 'KhmerOS_content', 'Battambang', 'Hanuman', sans-serif;
}
/* Smooth transitions */
.o_main_navbar,
.o_menu_navbar,
.btn,
.form-control {
transition: all 0.3s ease;
}
/* Khmer text rendering */
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Utility classes
.khmer-font {
font-family: var(--khmer-font-family) !important;
}
.khmer-menu-bg {
background-color: var(--khmer-menu-bg) !important;
}
*/
@@ -0,0 +1,62 @@
.o_sub_menu{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
font-size:16px;
color: blue;
}
.oe_secondary_menu{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
font-size:14px;
color: black;
}
.o_content{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
.o_cp_buttons{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
.active{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
.o_cp_controller{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
header.o_navbar{
background-color: #5dda32;
}
header{
background-color: #5dda32;
}
.o_main_navbar{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
background-color: #5dda32;
}
.o_menu_sections{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
background-color: #5dda32;
}
.o-dropdown-item.dropdown-item.o-navigable.o_nav_entry{
background-color: #5dda32;
}
.o-dropdown.dropdown-toggle.dropdown{
background-color: #5dda32;
}
.breadcrumb{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
body{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
main.o_content{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
div.o_form_view_container{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
.o-dropdown-item.dropdown-item.o-navigable.o_nav_entry{
font-family: Khmer OS content; src:url(../fonts/KhmerOS_content.ttf) ;format('truetype');
}
.o-dropdown-item.dropdown-item.o-navigable{
font-family: Khmer OS content; src:url('/custom_template_khmer/static/src/fonts/KhmerOS_content.ttf') format('truetype');
}
@@ -0,0 +1,12 @@
<!-- custom_template_khmer/templates/debug_config.xml -->
<odoo>
<template id="debug_config" name="Debug Config">
<t t-set="config" t-value="env['custom.template.config'].sudo().search([], limit=1)"/>
<div style="position: fixed; top: 100px; right: 10px; background: red; color: white; padding: 10px; z-index: 9999;">
<strong>DEBUG:</strong><br/>
Config ID: <t t-esc="config.id if config else 'None'"/><br/>
Color: <t t-esc="config.menu_bg_color if config else 'None'"/><br/>
Font: <t t-esc="config.font_name if config else 'None'"/>
</div>
</template>
</odoo>
@@ -0,0 +1,117 @@
<!-- custom_template_khmer/templates/layout_inherit.xml -->
<odoo>
<!-- Backend Layout -->
<template id="web_layout_inherit" inherit_id="web.layout" name="Khmer Theme Backend">
<xpath expr="//head" position="inside">
<t t-call="custom_template_khmer.dynamic_css_injector"/>
</xpath>
</template>
<!-- Website Layout -->
<template id="website_layout_inherit" inherit_id="website.layout" name="Khmer Theme Website">
<xpath expr="//head" position="inside">
<t t-call="custom_template_khmer.dynamic_css_injector"/>
</xpath>
</template>
<!-- Dynamic CSS Injector -->
<template id="dynamic_css_injector" name="Dynamic CSS Injector">
<style type="text/css">
<t t-set="config" t-value="env['custom.theme.config'].sudo().search([], limit=1)"/>
<!-- 1. DYNAMIC FONT -->
<!-- Inside templates/layout_inherit.xml -->
<t t-if="config and config.font_file">
<!-- 1. Define the Font Face -->
@font-face {
/* Use the EXACT name from your Settings field */
font-family: '<t t-esc="config.font_name"/>';
/* Dynamic URL to your controller */
src: url('/custom_template_khmer/fonts?t=<t t-esc="config.id"/>') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* 2. Apply the Font Globally */
/* We simply use the name defined above. No src here! */
body,form,.o_cp_buttons,.oe_secondary_menu,
.o_web_client,.o_content,.o_main_navbar,o-dropdown-item.dropdown-item.o-navigable,
.o_form_view .oe_title > h1, .o_form_view .oe_title > .h1, .o_form_view .oe_title > h2, .o_form_view .oe_title > .h2, .o_form_view .oe_title > h3, .o_form_view .oe_title > .h3{
font-family: '<t t-esc="config.font_name"/>', Khmer-Font !important;
}
</t>
<!-- 2. DYNAMIC MENU COLOR -->
<t t-if="config and config.menu_bg_color">
:root {
--khmer-menu-bg: <t t-esc="config.menu_bg_color"/>;
}
.o_main_navbar,
.o_menu_navbar,
#o_main_navbar,
header.o_navbar,
.o_navbar,
.navbar-expand-md,
.o_menu_systray,
.o_menu_sections {
background-color: <t t-esc="config.menu_bg_color"/> !important;
background: <t t-esc="config.menu_bg_color"/> !important;
}
.o_menu_sections.d-flex.flex-grow-1.flex-shrink-1.w-0{
background-color: <t t-esc="config.menu_bg_color"/> !important;
background: <t t-esc="config.menu_bg_color"/> !important;
}
.o-dropdown-item.dropdown-item.o-navigable.o_nav_entry{
background-color: <t t-esc="config.menu_bg_color"/> !important;
background: <t t-esc="config.menu_bg_color"/> !important;
}
.o_main_navbar .o_nav_entry, .o_main_navbar .dropdown-toggle:not(.o-dropdown-toggle-custo){
background-color: <t t-esc="config.menu_bg_color"/> !important;
background: <t t-esc="config.menu_bg_color"/> !important;
}
.o_menu_brand,
.navbar-brand {
background-color: <t t-esc="config.menu_bg_color"/> !important;
color: #FFFFFF !important;
}
<!-- With background image -->
<t t-if="config.menu_bg_image">
.o_main_navbar,
.o_menu_navbar,
#o_main_navbar {
background-image: url('/custom_template_khmer/menu_image?t=<t t-esc="config.id"/>') !important;
background-size: cover !important;
background-blend-mode: overlay !important;
}
</t>
.o_navbar_main_menu,
.navbar-custom,
#wrapwrap header {
background-color: <t t-esc="config.menu_bg_color"/> !important;
}
</t>
<!-- 3. RESPONSIVE DESIGN -->
<t t-if="config and config.is_responsive">
@media (max-width: 768px) {
.o_main_navbar { min-height: 40px; font-size: 13px; }
.o_menu_brand { font-size: 16px !important; }
}
@media (min-width: 769px) and (max-width: 1024px) {
.o_main_navbar { min-height: 44px; font-size: 14px; }
}
@media (min-width: 1025px) {
.o_main_navbar { min-height: 46px; font-size: 15px; }
}
</t>
</style>
</template>
</odoo>
@@ -0,0 +1,68 @@
<odoo>
<record id="res_config_settings_view_form_inherit_khmer" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.khmer</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//app[@name='general_settings']" position="inside">
<!-- HIDDEN LINK FIELD (Required for saving) -->
<field name="khmer_theme_config_id" invisible="1"/>
<block title="🇰 Khmer Theme Customization" name="khmer_theme_settings">
<!-- Font Settings -->
<!-- Inside the setting block for Font -->
<setting id="khmer_font_setting" string="Custom Font" help="Upload .ttf font file">
<div class="content-group">
<!-- Font Name Field -->
<div class="mt16 row">
<label for="khmer_font_name" string="Font Family Name)" class="col-lg-3 o_light_label"/>
<field name="khmer_font_name" filename="khmer_font_file"/>
</div>
<!-- Font Upload Field with Filename Attribute -->
<div class="mt16 row">
<label for="khmer_font_file" string="Upload Font (.ttf)" class="col-lg-3 o_light_label"/>
<!-- Add filename="khmer_font_file_name" to track the real name -->
<field name="khmer_font_file" class="col-lg-4" widget="binary" filename="khmer_font_file_name"/>
</div>
<!-- Hidden field to store the actual filename (Required for the widget to work) -->
<field name="khmer_font_file_name" invisible="1"/>
<div class="text-muted small mt8">
<i class="fa fa-info-circle"/> The font name will be extracted automatically upon upload.
</div>
</div>
</setting>
<!-- Menu Settings -->
<setting id="khmer_menu_setting" string="Menu Header" help="Set color or image">
<div class="content-group">
<div class="mt16 row">
<label for="khmer_menu_bg_color" string="Color" class="col-lg-3 o_light_label"/>
<field name="khmer_menu_bg_color" class="col-lg-4" widget="color"/>
</div>
<div class="mt16 row">
<label for="khmer_menu_bg_image" string="Image" class="col-lg-3 o_light_label"/>
<field name="khmer_menu_bg_image" class="col-lg-4" widget="binary"/>
</div>
</div>
</setting>
<!-- Responsive -->
<setting id="khmer_responsive_setting" string="Responsive">
<div class="content-group">
<div class="mt16 row">
<label for="khmer_is_responsive" string="Enable" class="col-lg-3 o_light_label"/>
<field name="khmer_is_responsive" class="col-lg-4"/>
</div>
</div>
</setting>
</block>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,18 @@
<odoo>
<record id="view_custom_template_config_form" model="ir.ui.view">
<field name="name">custom.template.config.form</field>
<field name="model">custom.template.config</field>
<field name="arch" type="xml">
<form string="Theme Configuration">
<group>
<field name="name"/>
<field name="font_name"/>
<field name="font_file"/>
<field name="menu_bg_color"/>
<field name="menu_bg_image"/>
<field name="is_responsive"/>
</group>
</form>
</field>
</record>
</odoo>