This commit is contained in:
pengyuthon
2024-06-20 13:39:24 +08:00
parent 1f2746db85
commit 3ae1b45506
295 changed files with 32591 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
# sphinx build directories
_build/
# dotfiles
.*
!.gitignore
!.github
!.mailmap
# compiled python files
*.py[co]
__pycache__/
# setup.py egg_info
*.egg-info
# emacs backup files
*~
# hg stuff
*.orig
status
# odoo filestore
odoo/filestore
# maintenance migration scripts
odoo/addons/base/maintenance
# generated for windows installer?
install/win32/*.bat
install/win32/meta.py
# needed only when building for win32
setup/win32/static/less/
setup/win32/static/wkhtmltopdf/
setup/win32/static/postgresql*.exe
# js tooling
node_modules
package-lock.json
package.json
.husky
# various virtualenv
/bin/
/build/
/dist/
/include/
/lib/
/man/
/share/
/src/
# avoid adding it after generation from tsconfig command
tsconfig.json
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
+64
View File
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
{
'name': "anita_theme_base",
'summary': """
Base for themes, it support free login for odoo
""",
'description': """
Odoo Login,
Odoo login page,
Odoo login theme
Login,
Anita Theme Base,
Anita Theme,
Awesome Theme,
Multi tab theme,
Pop form theme
""",
'author': "openErpNext",
'website': "https://base.openerpnext.com",
'live_test_url': 'https://base.openerpnext.com',
'license': 'OPL-1',
'images': ['static/description/screen_shot.png', 'static/description/banner.png'],
'support': 'codercrax@gmail.com',
'maintainer': 'openErpNext',
'category': 'Theme/Backend',
'version': '17.0.0.2',
'installable': True,
'application': True,
'auto_install': False,
'depends': ['base', 'web'],
'data': [],
'assets': {
'web.assets_backend': [
'anita_theme_base/static/css/anita_variables.scss',
'anita_theme_base/static/css/mixins/_flex.scss',
'anita_theme_base/static/css/mixins/_box_shadow.scss',
'anita_theme_base/static/css/mixins/_clearfix.scss',
'anita_theme_base/static/css/mixins/_float.scss',
'anita_theme_base/static/css/mixins/_hover.scss',
'anita_theme_base/static/css/mixins/_gradients.scss',
# 'anita_theme_base/static/css/mixins/_buttons.scss',
'anita_theme_base/static/css/anita_functions.scss',
'anita_theme_base/static/css/anita_form_controls.scss',
# 'anita_theme_base/static/css/anita_buttons.scss',
# 'anita_theme_base/static/css/anita_form_view.scss',
# 'anita_theme_base/static/css/anita_list_view.scss',
'anita_theme_base/static/css/anita_scroll.scss',
'anita_theme_base/static/css/anita_misc.scss',
]
}
}
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import anita_view_extend
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
import inspect
from odoo import models, _
from odoo.exceptions import AccessError
from odoo.models import check_method_name
class AnitaViewExtend(models.Model):
'''
anita view extend current_theme_mode
'''
_inherit = 'ir.ui.view'
def _render_template(self, template, values=None):
"""
rewrite to change the title
:param template:
:param values:
:param engine:
:return:
"""
if template in ['web.login', 'web.webclient_bootstrap']:
if not values:
values = {}
values["title"] = self.env['ir.config_parameter'].sudo().get_param(
"anita_theme_base.window_default_title", "openErpNext")
return super(AnitaViewExtend, self)._render_template(
template, values=values)
+1
View File
@@ -0,0 +1 @@
this is base for anita themes
@@ -0,0 +1,89 @@
// Make the div behave like a button
.btn-group {
// Prevent double borders when buttons are next to each other
.btn + .btn,
.btn + .btn-group,
.btn-group + .btn,
.btn-group + .btn-group {
margin-left: -$btn-border-width;
}
}
.btn-group {
// Reset rounded corners
> .btn:not(:last-child):not(.dropdown-toggle),
> .btn-group:not(:last-child) > .btn {
@include border-right-radius(0);
}
> .btn:not(:first-child),
> .btn-group:not(:first-child) > .btn {
@include border-left-radius(0);
}
}
// Sizing
//
// Remix the default button sizing classes into new ones for easier manipulation.
.btn-group-sm > .btn { @extend .btn-sm; }
.btn-group-lg > .btn { @extend .btn-lg; }
//
// Split button dropdowns
//
.dropdown-toggle-split {
padding-right: $btn-padding-x * .75;
padding-left: $btn-padding-x * .75;
}
.btn-sm + .dropdown-toggle-split {
padding-right: $btn-padding-x-sm * .75;
padding-left: $btn-padding-x-sm * .75;
}
.btn-lg + .dropdown-toggle-split {
padding-right: $btn-padding-x-lg * .75;
padding-left: $btn-padding-x-lg * .75;
}
// The clickable button for toggling the menu
// Set the same inset shadow as the :active state
.btn-group.show .dropdown-toggle {
@include box-shadow($btn-active-box-shadow);
// Show no shadow for `.btn-link` since it has no other button styles.
&.btn-link {
@include box-shadow(none);
}
}
//
// Vertical button groups
//
.btn-group-vertical {
> .btn + .btn,
> .btn + .btn-group,
> .btn-group + .btn,
> .btn-group + .btn-group {
margin-top: -$btn-border-width;
}
// Reset rounded corners
> .btn:not(:last-child):not(.dropdown-toggle),
> .btn-group:not(:last-child) > .btn {
@include border-bottom-radius(0);
}
> .btn:not(:first-child),
> .btn-group:not(:first-child) > .btn {
@include border-top-radius(0);
}
}
@@ -0,0 +1,140 @@
.btn {
font-weight: $btn-font-weight;
// font-family: $btn-font-family;
border: $input-btn-border-width solid transparent;
@include button-size(
$btn-padding-y,
$btn-padding-x,
$btn-font-size,
$input-btn-line-height,
$btn-border-radius
);
@include transition($btn-transition);
&:hover,
&.hover {
cursor: pointer;
}
&:focus,
&.focus {
@include box-shadow(none);
}
&:not([disabled]):not(.disabled):active,
&:not([disabled]):not(.disabled).active {
background-image: none;
@include box-shadow(none);
}
&.btn-squared {
border-radius: 0;
}
&.btn-pill {
// border-radius: $btn-pill-border-radius;
}
}
//
// Alternate buttons
//
@each $color, $value in $theme-colors {
.btn-#{$color} {
@include button-variant($value);
}
}
// White buttons
.btn-white {
@include button-variant($white);
}
// Black buttons
.btn-black {
@include button-variant($black);
}
// Outlined default buttons
@each $color, $value in $theme-colors {
.btn-outline-#{$color} {
@include button-outline-variant($value);
}
}
// White outline buttons
.btn-outline-white {
@include button-outline-variant($white);
color: $white;
&:not(:disabled):not(.disabled):active,
&:not(:disabled):not(.disabled).active {
color: $black;
}
}
// Black outline buttons
.btn-outline-black {
@include button-outline-variant($black);
color: $black;
&:not(:disabled):not(.disabled):active,
&:not(:disabled):not(.disabled).active {
color: $white;
}
}
// Make a button look and behave like a link
.btn-link {
font-weight: $font-weight-normal;
color: $link-color;
@include hover {
color: $link-hover-color;
text-decoration: $link-hover-decoration;
}
&:focus,
&.focus {
text-decoration: $link-hover-decoration;
}
&:disabled {
color: $btn-link-disabled-color;
}
}
//
// Button Sizes
//
.btn-lg {
@include button-size(
$btn-padding-y-lg,
$btn-padding-x-lg,
$btn-font-size-lg,
$btn-line-height-lg,
$btn-border-radius-lg
);
}
.btn-sm {
@include button-size(
$btn-padding-y-sm,
$btn-padding-x-sm,
$btn-font-size-sm,
$btn-line-height-sm,
$btn-border-radius-sm
);
}
// Vertically space out multiple block buttons
.btn-block + .btn-block {
margin-top: $btn-block-spacing-y;
}
@@ -0,0 +1,382 @@
// Checkboxes and radios
.custom-control {
min-height: (1 * $line-height-base);
padding-left: $custom-control-gutter;
&:hover {
cursor: pointer;
}
.custom-control-label:before {
pointer-events: all;
}
}
.custom-control-inline {
margin-right: $custom-control-spacer-x;
}
.custom-control-input {
&:checked~.custom-control-label::before {
color: $custom-control-indicator-checked-color;
border-color: transparent;
// @include gradient-bg($custom-control-indicator-checked-bg);
box-shadow: $custom-control-indicator-checked-box-shadow;
}
&:focus~.custom-control-label::before {
// the mixin is not used here to make sure there is feedback
// box-shadow: $custom-control-indicator-focus-box-shadow;
}
&:active~.custom-control-label::before {
color: $custom-control-indicator-active-color;
background-color: $custom-control-indicator-active-bg;
box-shadow: $custom-control-indicator-active-box-shadow;
}
&:disabled {
~.custom-control-label {
color: $custom-control-description-disabled-color; // $custom-control-label-disabled-color
&:hover {
cursor: not-allowed;
}
&::before {
background-color: $custom-control-indicator-disabled-bg;
}
}
}
}
// Custom control indicators
// Build the custom controls out of psuedo-elements.
.custom-control-label {
position: static;
&:hover {
cursor: pointer;
}
// Background-color and (when enabled) gradient
&::before {
top: ($line-height-base - $custom-control-indicator-size) / 2.0;
left: 0 !important;
width: $custom-control-indicator-size;
height: $custom-control-indicator-size;
background-color: $custom-control-indicator-bg;
border: $custom-control-indicator-border;
transition: all 250ms cubic-bezier(.27, .01, .38, 1.06);
box-shadow: $custom-control-indicator-box-shadow;
}
// Foreground (icon)
&::after {
top: ($line-height-base - $custom-control-indicator-size) / 2.0;
width: $custom-control-indicator-size;
height: $custom-control-indicator-size;
background-size: $custom-control-indicator-bg-size;
}
}
// Custom Switches (core)
// Adjustments for the default Bootstrap custom switches.
.custom-switch {
padding-left: $custom-switch-padding-left;
.custom-control-label {
position: relative;
::before {
left: -2.25rem !important;
width: 1.75rem !important;
}
}
}
// Checkboxes
.custom-checkbox {
.custom-control-label::before {
@include border-radius($custom-checkbox-indicator-border-radius);
}
.custom-control-label::after {
content: '';
position: absolute;
top: ($custom-control-indicator-size - 0.9166rem) / 2.0;
left: ($custom-control-indicator-size - 0.4166rem) / 2.0;
width: 5px;
height: 11px;
opacity: 0;
transform: rotate(45deg) scale(0);
border-right: 2px solid $white;
border-bottom: 2px solid $white;
transition: $custom-checkbox-indicator-transition;
transition-delay: 100ms;
}
.custom-control-input:checked~.custom-control-label {
// &::before {
// background-image: none;
// }
&::after {
opacity: 1;
transform: rotate(45deg) scale(1);
background-image: $custom-checkbox-indicator-icon-checked;
}
}
.custom-control-input:indeterminate~.custom-control-label {
&::before {
border: none;
// @include gradient-bg($custom-checkbox-indicator-indeterminate-bg);
box-shadow: $custom-checkbox-indicator-indeterminate-box-shadow;
}
&::after {
content: '';
position: absolute;
transform: scale(1);
background-image: $custom-checkbox-indicator-icon-indeterminate;
background-color: $white;
border: none;
width: 10px;
height: 2px;
top: 11px;
left: 4px;
opacity: 1;
transition: none;
}
}
.custom-control-input:disabled:checked~.custom-control-label {
&::before {
background: $custom-control-indicator-disabled-bg;
border-color: $border-color;
}
&::after {
border-color: $border-color;
}
}
}
// Radios
.custom-radio {
.custom-control-label::before {
border-radius: $custom-radio-indicator-border-radius;
}
.custom-control-label::after {
content: '';
border-radius: 50%;
transform: scale(0);
background-image: none !important;
position: absolute;
background: #fff;
width: 8px;
height: 8px;
top: (($line-height-base - 8 / 12.0) / 2.0);
left: (($custom-control-indicator-size - 8 / 12.0) / 2.0);
transition: all 250ms cubic-bezier(.27, .01, .38, 1.06);
transition-delay: .1s;
opacity: 0;
transform: scale(0);
}
.custom-control-input:checked~.custom-control-label {
&::before {
//@include gradient-bg($custom-control-indicator-checked-bg);
}
&::after {
opacity: 1;
transform: scale(1);
}
}
.custom-control-input:disabled {
&:checked~.custom-control-label::before {
background-color: lighten($blueish-grey, 30%);
}
}
.custom-control-input:disabled:checked~.custom-control-label {
&::before {
background: $custom-control-indicator-disabled-bg;
border-color: $border-color;
}
&::after {
background: $border-color;
}
}
}
// Custom toggle
.custom-toggle {
position: relative;
padding-left: ($custom-toggle-width + 0.625rem);
.custom-control-label {
// Background
&::before {
position: absolute;
top: 0;
left: 0;
display: block;
width: $custom-toggle-width;
height: $custom-toggle-height;
background: $custom-toggle-background-color;
border-radius: $custom-toggle-border-radius;
border: $custom-toggle-border;
}
// Knob
&::after {
content: '';
position: absolute;
top: 0.25rem;
left: 0.25rem;
width: $custom-toggle-knob-width;
height: $custom-toggle-knob-height;
background: $input-border-color;
border-radius: $custom-toggle-knob-border-radius;
transition: 350ms;
}
}
// Checked state
.custom-control-input:checked~.custom-control-label {
// Background
&::before {
background: $custom-toggle-checked-background;
border-color: $custom-toggle-checked-border-color;
}
// Knob
&::after {
left: $custom-toggle-width - $custom-toggle-border-width - 0.1875rem;
transform: translateX(-100%);
background: $custom-toggle-checked-knob-background;
}
}
// Disabled and checked state
.custom-control-input:checked:disabled~.custom-control-label {
&::before {
background: $custom-control-indicator-disabled-bg;
border-color: $border-color;
}
&::after {
background: $border-color;
}
}
// Active state
.custom-control-input:active:not(:disabled)~.custom-control-label {
// Knob
&::after {
width: $custom-toggle-knob-active-width;
}
}
// Active, but not checked.
.custom-control-input:active:not(:checked)~.custom-control-label {
&::before {
background-color: $custom-toggle-background-color;
}
}
// Disabled and active.
.custom-control-input:disabled:active~.custom-control-label {
&::before {
background-color: $custom-control-indicator-disabled-bg;
}
}
// Focus state
.custom-control-input:focus {
&~.custom-control-label {
&::before {
// box-shadow: $custom-toggle-focus-box-shadow;
}
}
&:not(:checked)~.custom-control-label {
&::before {
// box-shadow: $input-focus-box-shadow;
}
}
}
&.custom-toggle-sm {
padding-left: 2.625rem;
.custom-control-label {
// Background
&::before {
top: .1875rem;
position: absolute;
display: block;
width: $custom-toggle-width-sm;
height: $custom-toggle-height-sm;
background: $custom-toggle-background-color;
border-radius: $custom-toggle-border-radius;
border: $custom-toggle-border;
}
// Knob
&::after {
content: '';
position: absolute;
top: .375rem;
left: 0.1875rem;
width: $custom-toggle-knob-width-sm;
height: $custom-toggle-knob-height-sm;
}
}
// Checked state
.custom-control-input:checked~.custom-control-label {
// Knob
&::after {
left: $custom-toggle-width-sm - $custom-toggle-border-width - 0.1875rem;
}
}
// Active state
.custom-control-input:active:not(:disabled)~.custom-control-label {
// Knob
&::after {
width: $custom-toggle-knob-active-width-sm;
}
}
}
}
.o_boolean_toggle {
.custom-control.custom-checkbox>label.custom-control-label {
&::before {
width: 2.6375rem;
max-height: 20px;
}
i.fa-check-circle {
right: 3px;
}
}
}
@@ -0,0 +1,838 @@
// form
$o-form-lightsecondary: lighten($o-brand-secondary, 25%) !default;
$o-chatter-min-width: 530px !default;
$o-form-sheet-min-width: 990px !default;
$o-font-size-base-touch: 12px;
.o_input,
.single-line,
.js_import_options input {
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
font-size: inherit;
height: 34px;
line-height: 34px;
outline: none;
padding: 0 10px;
width: 100%;
&:focus {
border-color: #66afe9;
outline: 0;
}
&:hover {
border-color: #c0c4cc;
}
input {
&.o_input {
height: 30px;
}
}
}
.o_form_view {
background-color: transparent;
.o_form_sheet_bg {
border-bottom: 1px solid #ddd;
background: transparent
}
.o_group {
.o_td_label {
border-right: none
}
}
.o_form_sheet_bg {
border: none;
background: transparent;
>.o_form_sheet {
min-width: inherit;
}
}
.o_form_statusbar {
@include media-breakpoint-down(sm) {
padding-left: 0px;
}
>.o_statusbar_status {
>.o_arrow_button.btn-primary.disabled:after {
border-left-color: #007bff;
}
}
.o_statusbar_buttons_dropdown.dropdown-toggle {
width: 80px;
padding-left: 0px;
border: none;
border-right: 1px solid;
border-radius: 0px;
}
.dropdown-menu {
padding: 5px;
button {
margin-bottom: 5px;
width: 150px;
text-overflow: ellipsis;
}
}
}
.oe_button_box {
+.oe_title,
+.oe_avatar+.oe_title {
width: auto;
}
}
.o_cp_pager {
margin-right: 5px;
display: flex;
align-items: center;
.o_pager_counter {
margin: 5px;
padding: 5px;
}
button {
max-height: 30px;
}
}
.table-responsive {
border-top: 1px solid #dee2e6;
}
.o_list_table {
border-top: 1px solid #dee2e6;
margin-top: 5px;
}
}
.o_form_view {
@include media-breakpoint-up(md) {
display: flex;
flex-flow: column nowrap;
min-height: 100%;
}
// Sheet
.o_form_sheet_bg {
flex: 1 0 auto;
border-bottom: 1px solid gray('300');
>.o_form_sheet {
@include make-container();
@include make-container-max-widths();
border: 1px solid gray('300');
box-shadow: 0 5px 20px -15px black;
margin: $o-sheet-vpadding * 0.2 auto;
@include media-breakpoint-up(md) {
margin: $o-sheet-vpadding*0.5 auto;
}
padding: $o-sheet-vpadding;
@include o-form-sheet-inner-right-padding;
@include o-form-sheet-inner-left-padding;
// Selection
>.o_selection {
float: right;
}
}
}
// Title & avatar
.oe_title {
color: $headings-color;
@include media-breakpoint-up(vsm, $o-extra-grid-breakpoints) {
padding-right: $o-innergroup-rpadding;
}
}
.oe_avatar+.oe_title {
padding-right: $o-avatar-size + 10;
}
// Groups
.o_group {
// all groups take width 100% in mobile
@mixin o-generate-groups($n) {
@for $i from 1 through $n {
.o_group_col_#{$i} {
@include media-breakpoint-down(md) {
width: 100%;
}
}
}
}
@include o-generate-groups($o-form-group-cols);
&.o_inner_group {
>tbody>tr>td {
padding: 0 $o-innergroup-rpadding 0 0;
}
}
@include media-breakpoint-up(vsm, $o-extra-grid-breakpoints) {
.o_field_widget {
&.o_text_overflow {
width: 1px !important;
// hack to make the table layout believe it is a small element (so that the table does not grow too much) ...
min-width: 100%;
// ... but in fact it takes the whole table space
}
}
.o_form_label {
margin-bottom: $o-form-spacing-unit;
}
}
@include media-breakpoint-down(md) {
&.o_label_nowrap .o_form_label {
white-space: normal;
}
}
}
// Labels
.o_form_label {
margin: auto;
text-align: right;
&.o_form_label_empty,
&.o_form_label_false {
opacity: 0.5;
font-weight: normal;
}
@include media-breakpoint-down(sm) {
font-size: $o-font-size-base-touch;
}
}
// Chatter
>.oe_chatter,
.o_FormRenderer_chatterContainer {
flex: 1000 0 auto;
@include make-container();
// @include make-container-max-widths();
padding: $grid-gutter-width*0.25 0;
}
@include media-breakpoint-up(xl) {
>.oe_chatter,
.o_FormRenderer_chatterContainer {
max-width: 530px;
}
}
// Notebooks
.o_notebook {
>.o_notebook_headers {
// @include o-form-sheet-negative-margin;
>.nav.nav-tabs {
@include o-form-sheet-inner-left-padding;
//border-bottom: $nav-tabs-border-width solid gray('400');
>.nav-item {
white-space: nowrap;
>.nav-link {
color: $body-color;
//border-color: gray('400');
//background-color: white;
&:hover,
&:focus,
&:active {
outline: none;
color: $link-color;
}
&.active {
//border-bottom-color: white;
&,
&:hover,
&:focus,
&:active {
color: $headings-color;
//border-top-color: $o-brand-odoo;
}
}
&.disabled {
color: $text-muted;
}
}
}
}
}
>.tab-content {
border-bottom: 1px solid gray('400');
>.tab-pane {
> :first-child {
// Reset margin to 0 and use tab-pane's padding
// to define the distance between panel and content
margin-top: 0;
// These elements will appear attached to the tabs
&.oe_form_field_html {
@include o-form-sheet-negative-margin;
margin-top: -$o-horizontal-padding;
border: none;
.note-editor.panel {
border: none;
}
.note-toolbar.panel-heading {
@include o-form-sheet-inner-left-padding;
@include o-form-sheet-inner-right-padding;
border-top: none;
padding-top: $o-horizontal-padding*0.3;
// background: white;
.btn-secondary {
@include o-hover-text-color($body-color, $link-color);
}
}
.note-editing-area,
.o_readonly {
padding: $card-spacer-y $card-spacer-x;
@include o-form-sheet-inner-left-padding;
@include o-form-sheet-inner-right-padding;
}
// If immediatly followed by an .oe_clear element, the note-editor it's the 'only'
// tab's element. Reset margins to push the bar at the bottom.
+.oe_clear:last-child {
margin-bottom: -$o-horizontal-padding - $o-sheet-vpadding - $o-form-spacing-unit;
}
}
&.o_field_x2many.o_field_x2many_list {
display: block;
width: auto;
@include o-form-sheet-negative-margin;
margin-top: -$o-horizontal-padding;
.o_pager {
button {
min-width: 34px;
}
}
.table-responsive {
border-top: 1px solid #dee2e6
}
// use original padding-left for handle cell in editable list
tr> :first-child.o_handle_cell {
padding-left: $o-horizontal-padding;
}
tr> :first-child {
@include o-form-sheet-inner-left-padding;
}
tr> :last-child {
@include o-form-sheet-inner-right-padding;
}
}
}
}
}
&:last-child>.tab-content {
border-bottom: none;
}
}
// Notebooks for small screen
@include media-breakpoint-down(sm) {
.o_notebook .o_notebook_headers {
overflow-x: auto;
&::-webkit-scrollbar {
display: none;
}
.nav.nav-tabs {
flex-flow: row nowrap;
}
}
.o_cp_buttons {
width: 100%;
div,
.o-kanban-button-new {
width: 100%;
}
}
.o_cp_action_menus {
padding: 5px;
}
}
// One2Many List views
.o_field_widget {
.o_list_table {
&.table-striped {
>tbody {
>tr:not(.o_data_row)>td {
border-top: none;
}
}
// Show "border" if tfoot has content only
>tfoot>tr {
// box-shadow: inset 0 1px 0 gray('300');
>td {
border: none;
&:empty {
padding: 0;
}
}
}
}
}
}
// Translate icon
.o_field_translate {
padding-right: 0;
}
// Specific style classes
.o_group.o_inner_group.oe_subtotal_footer {
border-top: 1px solid gray('300');
.oe_subtotal_footer_separator {
border-top: 1px solid gray('300');
}
}
// Status Bar
.o_form_statusbar {
position: sticky;
top: 0;
z-index: 1;
>.o_statusbar_status {
.dropdown-menu {
.o_arrow_button {
font-weight: 500;
padding: 0.75rem;
&.btn-primary.disabled {
font-weight: bold;
}
// Last element at the right should respect overall padding
&:first-of-type {
padding-right: $o-horizontal-padding;
}
}
}
}
}
@include media-breakpoint-up(xxl, $o-extra-grid-breakpoints) {
// flex-flow: row nowrap;
height: 100%;
>.o_form_sheet_bg {
flex: 1 1 auto; // Side chatter is disabled if this has to shrink but this was added for safety
// width: $o-form-sheet-min-width + 2 * $o-horizontal-padding;
// max-width: map-get($container-max-widths, xl) + 2 * $o-horizontal-padding; // would be logical but breaks no-chatter form views
padding: 0 $o-horizontal-padding;
overflow: auto;
border-bottom: none;
> :not(.o_form_sheet) {
margin-left: -$o-horizontal-padding;
margin-right: -$o-horizontal-padding;
}
>.o_form_sheet {
width: 100%;
max-width: map-get($container-max-widths, xl);
}
}
>.oe_chatter,
.o_FormRenderer_chatterContainer {
// flex: 1 1 auto; // Side chatter is disabled if this has to shrink but this was added for safety
width: $o-chatter-min-width;
max-width: 530px;
padding: 0;
overflow: auto;
border-left: 1px solid gray('300');
background-color: white;
.o_chatter_topbar {
height: $o-statusbar-height + $nav-tabs-border-width;
.btn,
.btn.o_active {
border-top: 0;
}
}
&.o_chatter_composer_active {
.o_chatter_topbar {
.btn,
.btn.o_active {
border-top: 0;
&:first-child {
border-left: 0;
}
}
}
}
.o_thread_composer {
border-width: 1px 0;
margin-top: 0;
// have mention drowdown below composer
.o_composer_mention_dropdown {
position: absolute;
top: 100%;
>.dropdown-menu {
top: 100%;
bottom: auto;
}
}
}
}
}
.o_field_widget {
&.o_field_many2one {
.o_external_button {
background: transparent;
min-width: 34px;
&:hover {
background: transparent;
}
&:focus {
outline: none;
background: transparent;
}
&:active {
background: transparent;
}
}
}
.o_input_dropdown>input {
height: 34px;
line-height: 34px;
padding-right: 15px;
cursor: pointer;
}
.o_field_color {
width: 24px;
height: 24px;
border-radius: 5px;
}
.o_input_dropdown {
.o_dropdown_arrow {
position: absolute;
line-height: 34px;
top: 0px;
bottom: 0px;
left: auto;
bottom: auto;
right: 8px;
color: #c0c4cc;
font-size: 14px;
cursor: pointer;
pointer-events: none;
-webkit-transition: -webkit-transform .3s;
transition: -webkit-transform .3s;
transition: transform .3s;
transition: transform .3s, -webkit-transform .3s;
-webkit-transform: rotateZ(180deg);
transform: rotateZ(180deg);
&.anita_reverse {
-webkit-transform: rotateZ(0);
transform: rotateZ(0)
}
&.dropdown_reverse {
-webkit-transform: rotateZ(0);
transform: rotateZ(0)
}
}
}
&.o_field_many2manytags {
overflow: hidden;
align-items: center;
.o_input_dropdown {
.o_dropdown_arrow {
right: 3px;
}
}
}
&.o_form_uri {
line-height: 36px;
}
}
}
// Overriden style when form view in modal
.modal .modal-dialog {
.o_form_view {
@include media-breakpoint-down(md) {
.o_group.o_inner_group>tbody>tr>td.o_td_label {
min-width: 100px;
}
}
}
&.modal-sm .o_form_view {
.o_group {
@include form-break-table;
}
}
}
// XXS form view specific rules
.o_form_view.o_xxs_form_view {
.o_group {
// Target XXS form view on mobile devices in portrait mode
@include media-breakpoint-down(sm) {
&.o_inner_group>tbody>tr>td {
.o_field_widget {
margin-bottom: $o-form-spacing-unit * 4;
}
}
}
.o_td_label .o_form_label {
font-weight: normal;
}
}
.o_form_label {
margin-top: 3px;
font-size: $o-label-font-size-factor * $o-font-size-base-touch;
font-weight: normal;
color: #6c757d;
&.o_form_label_empty {
display: none;
}
}
}
.form-control:focus {
color: #495057;
background-color: #FFFFFF;
border-color: #007bff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(233, 236, 239, 0.25);
}
.ui-autocomplete {
margin-top: 3px;
}
.o_setting_right_pane {
.o_web_settings_invite {
margin: 8px;
}
}
.o_external_button {
background: none;
border: none;
&:hover {
background: none;
border: none;
box-shadow: none;
}
}
.note-editor {
border: 1px solid #dcdfe6 !important;
.panel-heading {
&.note-toolbar {
border-bottom: 1px solid #dcdfe6;
background-color: #ffffff;
color: #212529;
.btn {
border-left: none;
border-right: none;
background: transparent;
}
.dropdown-item {
background-color: #595964;
color: #C6C6C6;
}
.btn-group.show>.btn {
background: #dee2e6;
color: rgba(76, 76, 76, 0.82);
}
.dropdown-item:not(.d-none)~.dropdown-item {
border-top: none;
}
}
}
}
.o_input_dropdown {
.o_dropdown_arrow {
position: absolute;
line-height: 34px;
top: 0px;
bottom: 0px;
left: auto;
bottom: auto;
right: 8px;
color: #c0c4cc;
font-size: 14px;
cursor: pointer;
pointer-events: none;
-webkit-transition: -webkit-transform .3s;
transition: -webkit-transform .3s;
transition: transform .3s;
transition: transform .3s, -webkit-transform .3s;
-webkit-transform: rotateZ(180deg);
transform: rotateZ(180deg);
}
}
.o_field_binary_file {
.btn,
button {
margin-left: 5px;
}
}
.nav-tabs {
.nav-link.active,
.nav-item.show .nav-link {
color: #495057;
background-color: #FFFFFF;
border-color: #dee2e6 #dee2e6 #FFFFFF;
}
}
.o_field_binary_file>.btn {
padding: 0px 10px;
height: 34px;
line-height: 34px;
}
.o_required_modifier.o_input,
.o_required_modifier .o_input {
background-color: #f8f8ff !important;
}
.oe_title {
h1 {
padding: 10px 0px;
}
}
.o_form_view .o_form_statusbar>.o_statusbar_status>.o_arrow_button.btn-primary.disabled {
background-color: #006fe6;
}
.note-popover .popover .popover-body .dropdown-menu,
.panel-heading.note-toolbar .dropdown-menu {
border: none;
background-color: inherit;
border-color: inherit;
box-shadow: inherit;
}
.o_field_x2many_list {
border: 1px solid #dcdfe6;
.o_list_table {
border-top: none;
margin-top: 0px;
}
}
.o_form_view {
.o_notebook {
.tab-content {
padding: 10px
}
}
}
.o_boolean_toggle {
&.custom-control.custom-checkbox>label.custom-control-label::after {
content: none !important;
}
&.custom-control.custom-checkbox>label.custom-control-label::before,
&.custom-control.custom-checkbox>label.custom-control-label::after {
left: 1px !important;
}
}
@@ -0,0 +1,91 @@
// Bootstrap functions
//
// Utility mixins and functions for evalutating source code across our variables, maps, and mixins.
// Ascending
// Used to evaluate Sass maps like our grid breakpoints.
@mixin _assert-ascending($map, $map-name) {
$prev-key: null;
$prev-num: null;
@each $key, $num in $map {
@if $prev-num == null {
// Do nothing
} @else if not comparable($prev-num, $num) {
@warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !";
} @else if $prev-num >= $num {
@warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !";
}
$prev-key: $key;
$prev-num: $num;
}
}
// Starts at zero
// Another grid mixin that ensures the min-width of the lowest breakpoint starts at 0.
@mixin _assert-starts-at-zero($map) {
$values: map-values($map);
$first-value: nth($values, 1);
@if $first-value != 0 {
@warn "First breakpoint in `$grid-breakpoints` must start at 0, but starts at #{$first-value}.";
}
}
// Replace `$search` with `$replace` in `$string`
// Used on our SVG icon backgrounds for custom forms.
//
// @author Hugo Giraudel
// @param {String} $string - Initial string
// @param {String} $search - Substring to replace
// @param {String} $replace ('') - New value
// @return {String} - Updated string
@function str-replace($string, $search, $replace: "") {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
// Color contrast
@mixin color-yiq($color) {
$r: red($color);
$g: green($color);
$b: blue($color);
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
@if ($yiq >= 150) {
color: darken($color, 65);
} @else {
color: #fff;
}
}
// Retreive color Sass maps
@function color($key: "blue") {
@return map-get($colors, $key);
}
@function theme-color($key: "primary") {
@return map-get($theme-colors, $key);
}
@function grayscale($key: "100") {
@return map-get($grays, $key);
}
// Request a theme color level
@function theme-color-level($color-name: "primary", $level: 0) {
$color: theme-color($color-name);
$color-base: if($level > 0, #000, #fff);
@if $level < 0 {
// Lighter values need a quick double negative for the Sass math to work
@return mix($color-base, $color, $level * -1 * $theme-color-interval);
} @else {
@return mix($color-base, $color, $level * $theme-color-interval);
}
}
@@ -0,0 +1,229 @@
html .o_web_client > .o_action_manager > .o_action .o_content {
display: flex;
flex-direction: column;
&.o_component_with_search_panel {
flex-direction: row !important;
}
}
.o_list_view {
overflow: auto;
flex: 1 1 auto;
.custom-checkbox {
.custom-control-label::before {
border-radius: 2px;
}
}
thead>tr>th:nth-of-type(1) {
border-left: none;
}
.o_list_table {
tbody>tr>td:not(.o_list_record_selector):not(.o_handle_cell) {
vertical-align: middle !important;
}
.o_list_record_selector {
vertical-align: middle !important;
}
}
.table-responsive {
.o_list_table {
thead {
box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.1);
>tr {
min-height: 36px;
height: 36px;
}
}
thead,
thead tr th {
position: sticky !important;
top: 0;
z-index: 1;
background: white;
}
.o_list_record_selector {
width: 40px;
padding-left: 16px;
}
tfoot,
tfoot>tr>td {
position: sticky;
bottom: 0;
background: white;
background-color: $o-list-footer-bg-color;
}
.o_list_record_selector {
vertical-align: middle;
}
.o_column_sortable:not(.o_handle_cell) {
position: relative;
user-select: none;
}
tfoot {
background-color: none !important;
}
.o_data_row {
border-bottom-width: 1px;
border-bottom-color: #dcdfe6;
&.o_selected_row>.o_data_cell:not(.o_readonly_modifier):not(.o_invisible_modifier) {
background-color: transparent;
&:not(.o_remaining_days_cell) .o_input {
border: 1px solid #dcdfe6 !important;
height: 32px;
line-height: 32px;
}
}
}
thead {
>tr {
>th {
padding: 5px 4px !important;
border-top: none;
border-bottom: none;
&:not(.o_list_record_selector) {
border-left: none;
vertical-align: middle;
}
&:nth-of-type(1) {
border-left: none;
padding: 0px 10px !important;
}
}
}
}
// add the shaddow
tbody {
>tr {
>td {
// padding: 0px 10px !important;
&:not(.o_list_record_selector) {
&.o_list_button>button {
padding: 0.4rem 1.2rem;
}
&:not(.o_handle_cell) {
vertical-align: middle;
}
}
}
&:nth-of-type(odd) {
background-color: rgba(0, 0, 0, 0.01);
}
}
}
.o_list_record_remove {
padding-right: 0px !important;
line-height: 40px;
}
}
.o_field_cell,
.o_data_cell,
.o_column_sortable {
height: 30px;
line-height: 30px;
padding: 0 10px;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
}
.o_optional_columns_dropdown_toggle,
.o_optional_columns {
top: 5px;
right: 5px;
}
.o_field_badge {
margin-top: 8px;
margin-left: 5px;
}
.o_optional_columns_dropdown_toggle,
.o_optional_columns {
top: 5px;
right: 5px;
}
}
.o_list_view {
.o_list_table {
tfoot {
background-color: none !important;
border-bottom: 1px solid #dcdfe6;
}
.o_data_row.o_selected_row>.o_data_cell:not(.o_readonly_modifier):not(.o_invisible_modifier) {
background-color: transparent;
}
thead>tr>th:not(.o_list_record_selector) {
border-left: none;
vertical-align: middle;
}
&.table-bordered {
border-collapse: collapse;
.tfoot {
border-top: none;
}
}
}
.o_field_x2many_list_row_add {
list-style: 30px;
height: 30px;
}
}
.o_list_buttons {
button {
margin-right: 5px;
}
}
.o_form_sheet {
.o_list_view {
.o_list_table {
thead {
box-shadow: none !important;
border-bottom: 1px solid #dcdfe6;
}
}
}
}
.table > :not(caption) > * > * {
border-bottom-color: #dcdfe6;
}
@@ -0,0 +1,23 @@
.o_filter_condition {
select:not(:first-child) {
margin-top: 5px;
}
}
.row {
.o_field_widget.o_readonly_modifier {
padding-top: calc(0.375rem + 1px);
padding-bottom: calc(0.375rem + 1px - 5px);
}
}
.o_input input {
border: none !important;
}
.o_form_status_indicator_buttons {
button {
margin-right: 0.5rem;
}
}
@@ -0,0 +1,24 @@
// scroll bar
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #FFFFFF;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:increment,
::-webkit-scrollbar-thumb:decrement {
background: #000;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
@@ -0,0 +1,327 @@
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #868e96 !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
$grays: () !default;
$grays: map-merge((
"100": $gray-100,
"200": $gray-200,
"300": $gray-300,
"400": $gray-400,
"500": $gray-500,
"600": $gray-600,
"700": $gray-700,
"800": $gray-800,
"900": $gray-900
), $grays);
$blue: #007bff !default;
$indigo: #674eec !default;
$purple: #8445f7 !default;
$pink: #ff4169 !default;
$red: #c4183c !default;
$orange: #fb7906 !default;
$yellow: #ffb400 !default;
$green: #17c671 !default;
$teal: #1adba2 !default;
$cyan: #00b8d8 !default;
$colors: () !default;
$colors: map-merge((
"blue": $blue,
"indigo": $indigo,
"purple": $purple,
"pink": $pink,
"red": $red,
"orange": $orange,
"yellow": $yellow,
"green": $green,
"teal": $teal,
"cyan": $cyan,
"white": $white,
"gray": $gray-600,
"gray-dark": $gray-800
), $colors);
$primary: $blue !default;
$secondary: $blueish-grey !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-200 !default;
$dark: $gray-900 !default;
// Options
$enable-rounded: true !default;
$enable-shadows: true !default;
$enable-gradients: false !default;
$enable-transitions: true !default;
$enable-hover-media-query: false !default;
$enable-fonts-import: true !default;
$theme-colors: () !default;
$theme-colors: map-merge((
"primary": $primary,
"secondary": $secondary,
"success": $success,
"info": $info,
"warning": $warning,
"danger": $danger,
"light": $light,
"dark": $dark
), $theme-colors);
$blueish-grey: #5A6169 !default;
// Easings
$ease-in-out-circ: cubic-bezier(.27,.01,.38,1.06);
// Transitions
$transition-duration: 250ms;
$transition-base: all $transition-duration $ease-in-out-circ !default;
$transition-fade: opacity 200ms ease-in-out !default;
$transition-collapse: height 350ms ease-in-out !default;
// Fonts
$google-fonts-import: 'https://fonts.googleapis.com/css?family=Poppins:300,400,500,600|Roboto+Mono' !default; // Includes: Poppins: Light, Regular, Medium, Semi-bold; Roboto Mono: Regular
$font-family-poppins-first: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
$font-family-system-first: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
$font-family-roboto-mono-first: "Roboto Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default;
$font-family-sans-serif: $font-family-system-first !default;
$font-family-monospace: $font-family-roboto-mono-first !default;
$font-family-base: $font-family-sans-serif !default;
$font-size-base: 1rem !default;
$font-size-lg: 1.25rem !default;
$font-size-sm: .875rem !default;
$font-weight-normal: 300 !default;
$font-weight-bold: 500 !default;
$font-weight-base: $font-weight-normal !default;
$line-height-base: 1.5 !default;
// Buttons
// For each of Bootstrap's buttons, define text, background and border color.
$btn-padding-y-lg: .75rem !default;
$btn-padding-x-lg: 1.75rem !default;
$btn-line-height-sm: $line-height-sm !default;
$btn-line-height-lg: $line-height-lg !default;
$btn-padding-y-sm: .35rem !default;
$btn-padding-x-sm: 1rem !default;
$input-btn-padding-y: .5rem !default;
$input-btn-padding-x: 1rem !default;
$input-btn-line-height: 1.125 !default;
$input-btn-padding-y-sm: .35rem !default;
$input-btn-padding-x-sm: .75rem !default;
$input-btn-padding-y-lg: .75rem !default;
$input-btn-padding-x-lg: 1rem !default;
$btn-pill-border-radius: 50px !default;
$btn-padding-y: .75rem !default;
$btn-padding-x: 1.25rem !default;
$btn-font-weight: $font-weight-normal !default;
$btn-font-size: .875rem !default;
$btn-font-size-sm: 0.75rem !default;
$btn-font-size-lg: 1.125rem !default;
$btn-font-family: $font-family-poppins-first !default;
$btn-box-shadow: none !default;
$input-btn-focus-width: .2rem !default;
$btn-active-box-shadow: inset 0 3px 5px rgba($black,.125) !default;
$btn-link-disabled-color: $gray-600 !default;
$btn-block-spacing-y: .5rem !default;
// Allows for customizing button radius independently from global border radius
$btn-border-radius: $border-radius !default;
$btn-border-radius-lg: $border-radius-lg !default;
$btn-border-radius-sm: $border-radius-sm !default;
$btn-border-width: 1px !default;
$btn-transition: $transition-base !default;
// Forms
$input-padding-y: $input-btn-padding-y !default;
$input-padding-x: $input-btn-padding-x !default;
$input-line-height: 1.5 !default;
$input-padding-y-sm: $input-btn-padding-y-sm !default;
$input-padding-x-sm: $input-btn-padding-x-sm !default;
$input-line-height-sm: 1.5 !default;
$input-padding-y-lg: $input-btn-padding-y-lg !default;
$input-padding-x-lg: $input-btn-padding-x-lg !default;
$input-line-height-lg: 1.5 !default;
$input-bg: $white !default;
$input-disabled-bg: #f5f6f7 !default;
$input-font-size: .95rem !default;
$input-color: $gray-700 !default;
$input-border-color: $border-color !default;
$input-btn-border-width: $border-width !default;
$input-box-shadow: none !default;
$input-border-width: $border-width !default;
$input-border-radius: $border-radius !default;
$input-border-radius-lg: $border-radius-lg !default;
$input-border-radius-sm: $border-radius-sm !default;
$input-focus-bg: $input-bg !default;
$input-focus-border-color: theme-color("primary") !default;
$input-focus-box-shadow: 0 .313rem .719rem rgba(theme-color("primary"),.1), 0 .156rem .125rem rgba($black, .06) !default;
$input-focus-color: $input-color !default;
$input-hover-border-color: darken($input-border-color, 15) !default;
$input-placeholder-color: $gray-600 !default;
$input-height-border: $input-border-width * 2 !default;
$input-height-inner: ($input-font-size * $input-line-height) + ($input-padding-y * 2) !default;
$input-height: calc(#{$input-height-inner} + #{$input-height-border}) !default;
$input-height-inner-sm: ($font-size-sm * $input-line-height-sm) + ($input-padding-y-sm * 2) !default;
$input-height-sm: calc(#{$input-height-inner-sm} + #{$input-height-border}) !default;
$input-height-inner-lg: ($font-size-lg * $input-line-height-lg) + ($input-padding-y-lg * 2) !default;
$input-height-lg: calc(#{$input-height-inner-lg} + #{$input-height-border}) !default;
$input-transition: box-shadow $transition-duration $ease-in-out-circ,
border $transition-duration $ease-in-out-circ !default;
$form-text-margin-top: .25rem !default;
$form-check-input-gutter: 1.25rem !default;
$form-check-input-margin-y: .313rem !default;
$form-check-input-margin-x: .313rem !default;
$form-check-inline-margin-x: .75rem !default;
$form-check-inline-input-margin-x: .3125rem !default;
$form-group-margin-bottom: 1rem !default;
$input-group-padding-y: 12px !default;
$input-group-padding-x: 14px !default;
$input-group-addon-bg: lighten($input-border-color, 19%) !default;
$input-group-addon-border-color: $input-border-color !default;
$input-group-addon-color: $input-frozen-color !default;
$custom-control-gutter: 1.688rem !default;
$custom-control-spacer-x: 1rem !default;
$custom-control-indicator-size: 1.125rem !default;
$custom-control-indicator-bg: $white !default;
$custom-control-indicator-bg-size: 50% 50% !default;
$custom-control-indicator-box-shadow: none !default;
$custom-control-indicator-border: 1px solid $border-color !default;
$custom-checkbox-indicator-transition: transform $transition-duration $ease-in-out-circ,
border $transition-duration $ease-in-out-circ !default;
$custom-control-indicator-disabled-bg: $gray-200 !default;
$custom-control-description-disabled-color: $gray-600 !default;
$custom-control-indicator-checked-color: $white !default;
$custom-control-indicator-checked-bg: theme-color("primary") !default;
$custom-control-indicator-checked-box-shadow: none !default;
$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;
$custom-control-indicator-active-color: $white !default;
$theme-color-primary: theme-color("primary") !default;
$custom-control-indicator-active-bg: lighten(#007bff, 35%) !default;
$custom-control-indicator-active-box-shadow: none !default;
// Custom checkbox
$custom-checkbox-indicator-border-radius: 2px !default;
$custom-checkbox-indicator-icon-checked: none !default;
$custom-checkbox-indicator-indeterminate-bg: theme-color("primary") !default;
$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;
$custom-checkbox-indicator-icon-indeterminate: none !default;
$custom-checkbox-indicator-indeterminate-box-shadow: none !default;
// Custom radio
$custom-radio-indicator-border-radius: 50% !default;
// Custom switches (core)
$custom-switch-padding-left: 2.25rem !default;
// Custom toggle
$custom-toggle-width: 3.125rem !default;
$custom-toggle-height: 1.75rem !default;
$custom-toggle-border-width: 0.0625rem !default;
$custom-toggle-border: $custom-toggle-border-width solid $input-border-color !default;
$custom-toggle-background-color: $white !default;
$custom-toggle-border-radius: 100px !default;
$custom-toggle-focus-box-shadow: 0 .313rem .719rem rgba($green,.1), 0 .156rem .125rem rgba($black, .06) !default;
$custom-toggle-checked-background: theme-color("success") !default;
$custom-toggle-checked-border-color: $custom-toggle-checked-background !default;
$custom-toggle-checked-knob-background: $white !default;
$custom-toggle-knob-width: 1.25rem !default;
$custom-toggle-knob-height: 1.25rem !default;
$custom-toggle-knob-border-radius: 6.25rem !default;
$custom-toggle-knob-active-width: 1.625rem !default;
$custom-toggle-invalid-knob-background-color: #eb8c95 !default;
$custom-toggle-invalid-background-color: $white !default;
$custom-toggle-width-sm : 2.1875rem !default;
$custom-toggle-height-sm : 1.125rem !default;
$custom-toggle-knob-width-sm: .75rem !default;
$custom-toggle-knob-active-width-sm: 1rem !default;
$custom-toggle-knob-height-sm: .75rem !default;
$body-bg: $white !default;
$body-color: $blueish-grey !default;
// awesome
$sidebar_toggle_one_width: 70px;
$sidebar_toggle_two_width: 240px;
$o-font-size-base-touch: 12px;
$custom-file-height: 30px;
$custom-file-width: 200px;
// form
$o-form-lightsecondary: lighten($o-brand-secondary, 25%) !default;
$o-tooltip-background-color: $o-view-background-color !default;
$o-tooltip-color: $o-main-text-color !default;
$o-tooltip-arrow-color: $o-main-text-color !default;
// Needed for having no spacing between sheet and mail body in mass_mailing:
// Different required cancel paddings between web and web_enterprise
$o-sheet-cancel-tpadding: $o-horizontal-padding !default;
$o-sheet-cancel-bpadding: $o-horizontal-padding + $o-sheet-vpadding !default;
// Side chatter
$o-chatter-min-width: 530px !default;
// Safest for the next value would be map-get($container-max-widths, lg) as it
// is the minimal width of the default form view design for md/lg sizes
$o-form-sheet-min-width: 990px !default;
+135
View File
@@ -0,0 +1,135 @@
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLDz8Z11lFc-K.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiEyp8kv8JHgFVrJJbecmNE.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLGT9Z11lFc-K.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLCz7Z11lFc-K.woff2') format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/anita_theme_base/static/css/login/fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@@ -0,0 +1,390 @@
html,
body {
height: 100%;
}
body {
overflow: auto;
margin: 0;
padding: 0;
background-color: #182139;
background-size: cover;
}
.form-control {
height: auto;
border: 1px solid #bfc9d4;
color: #3b3f5c;
font-size: 15px;
padding: 8px 10px;
letter-spacing: 1px;
height: calc(1.4em + 1.4rem + 2px);
padding: .75rem 1.25rem;
border-radius: 6px;
}
.form-control:focus {
box-shadow: 0 0 5px 2px rgba(194, 213, 255, 0.6196078431372549);
border-color: #1b55e2;
color: #3b3f5c;
}
.form-form {
width: 50%;
display: flex;
flex-direction: column;
min-height: 100%;
margin: 0 auto;
}
.form-form .form-form-wrap {
max-width: 480px;
margin: 0 auto;
min-width: 311px;
min-height: 100%;
align-self: center;
width: 100%;
height: 100vh;
justify-content: center;
}
.form-form .form-container {
align-items: center;
display: flex;
flex-grow: 1;
width: 100%;
min-height: 100%;
}
.form-form .form-container .form-content {
display: block;
width: 100%;
padding: 25px;
background: #fff;
text-align: center;
border-radius: 15px;
border: 1px solid #e0e6ed;
-webkit-box-shadow: 0 4px 6px 0 rgba(85, 85, 85, 0.09019607843137255), 0 1px 20px 0 rgba(0, 0, 0, 0.08), 0px 1px 11px 0px rgba(0, 0, 0, 0.06);
-moz-box-shadow: 0 4px 6px 0 rgba(85, 85, 85, 0.09019607843137255), 0 1px 20px 0 rgba(0, 0, 0, 0.08), 0px 1px 11px 0px rgba(0, 0, 0, 0.06);
box-shadow: 0 4px 6px 0 rgba(85, 85, 85, 0.09019607843137255), 0 1px 20px 0 rgba(0, 0, 0, 0.08), 0px 1px 11px 0px rgba(0, 0, 0, 0.06);
}
.form-form .form-container .form-content h1 {
font-size: 32px;
color: #3b3f5c;
}
.form-form .form-container .form-content>p {
font-size: 13px;
color: #888ea8;
font-weight: 600;
margin-bottom: 35px;
}
.form-form .form-form-wrap .user-meta {
margin-bottom: 35px;
}
.form-form .form-form-wrap .user-meta img {
width: 52px;
height: 52px;
border-radius: 50%;
margin-right: 15px;
border: 4px solid #e0e6ed;
}
.form-form .form-form-wrap .user-meta div {
align-self: center;
}
.form-form .form-form-wrap .user-meta p {
font-size: 31px;
color: #3b3f5c;
margin-bottom: 0;
}
.form-form .form-form-wrap h1 .brand-name {
color: #3b3f5c;
font-weight: 600;
}
.form-form .form-form-wrap p.signup-link {
font-size: 14px;
color: #3b3f5c;
font-weight: 700;
margin-bottom: 15px;
text-align: center;
margin-top: 50px;
}
.form-form .form-form-wrap p.signup-link.register {
font-size: 13px;
color: #888ea8;
font-weight: 600;
margin-bottom: 25px;
margin-top: 0;
}
.form-form .form-form-wrap p.signup-link.recovery {
margin-top: 0;
}
.form-form .form-form-wrap p.signup-link a {
color: #1b55e2;
border-bottom: 1px solid;
}
.form-form .form-form-wrap form .field-wrapper.input {
padding: 11px 0px 16px 0;
border-bottom: none;
position: relative;
}
.form-form .form-form-wrap form .field-wrapper.input:focus {
border: 1px solid #000;
}
.form-form .form-form-wrap form .field-wrapper.toggle-pass p {
font-weight: 600;
color: #3b3f5c;
margin-bottom: 0;
}
.form-form .form-form-wrap form .field-wrapper .logged-in-user-name {
font-size: 37px;
color: #3b3f5c;
}
.form-form .form-form-wrap form .field-wrapper label {
font-size: 10px;
font-weight: 700;
color: #3b3f5c;
margin-bottom: 8px;
}
.form-form .form-form-wrap form .field-wrapper.terms_condition {
margin-bottom: 20px;
}
.form-form .form-form-wrap form .field-wrapper.terms_condition label {
font-size: 14px;
color: #888ea8;
padding-left: 31px;
font-weight: 100;
}
.form-form .form-form-wrap form .field-wrapper.terms_condition a {
color: #1b55e2;
}
.form-form .form-form-wrap form .field-wrapper svg.feather-user {
top: 53px;
}
.form-form .form-form-wrap form .field-wrapper svg.feather-lock {
top: 48px;
}
.form-form .form-form-wrap form .field-wrapper svg.feather-at-sign {
top: 47px;
}
.form-form .form-form-wrap form .field-wrapper svg.feather-at-sign.register {
top: 53px;
}
.form-form .form-form-wrap form .field-wrapper svg:not(.feather-eye) {
position: absolute;
left: 12px;
color: #888ea8;
fill: rgba(0, 23, 55, 0.08);
width: 20px;
height: 20px;
}
.form-form .form-form-wrap form .field-wrapper svg.feather-eye {
position: absolute;
top: 46px;
right: 13px;
color: #888ea8;
fill: rgba(0, 23, 55, 0.08);
width: 17px;
cursor: pointer;
}
.form-form .form-form-wrap form .field-wrapper input {
display: inline-block;
vertical-align: middle;
border-radius: 6px;
min-width: 50px;
max-width: 635px;
width: 100%;
-ms-transition: all 0.2s ease-in-out 0s;
transition: all 0.2s ease-in-out 0s;
color: #3b3f5c;
font-weight: 600;
font-size: 16px;
padding: 13px 35px 13px 46px;
}
.form-form .form-form-wrap form .field-wrapper input::-webkit-input-placeholder {
color: #d3d3d3;
font-size: 14px;
}
.form-form .form-form-wrap form .field-wrapper input::-ms-input-placeholder {
color: #d3d3d3;
font-size: 14px;
}
.form-form .form-form-wrap form .field-wrapper input::-moz-placeholder {
color: #d3d3d3;
font-size: 14px;
}
.form-form .form-form-wrap form .field-wrapper {
width: 100%
}
.form-form .form-form-wrap form .field-wrapper button.btn {
align-self: center;
width: 100%;
padding: 11px 14px;
font-size: 16px;
letter-spacing: 2px;
}
.form-form .form-form-wrap form .field-wrapper a.forgot-pass-link {
font-weight: 600;
color: #888ea8;
display: block;
letter-spacing: 1px;
font-size: 12px;
margin-bottom: 8px;
}
.form-form .form-form-wrap form .field-wrapper .n-chk .new-control-indicator {
top: 2px;
border: 1px solid #bfc9d4;
background-color: #f1f2f3;
}
.form-form .form-form-wrap form .field-wrapper .n-chk .new-control-indicator:after {
top: 54%;
}
.new-control.new-checkbox.checkbox-primary>input:checked~span.new-control-indicator {
border: none;
}
.form-form .form-form-wrap form .division {
text-align: center;
font-size: 13px;
margin: 35px 0 38px 0;
}
.form-form .form-form-wrap form .social {
text-align: center;
}
.form-form .form-form-wrap form .social a {
background: transparent;
box-shadow: none;
border: 1px solid #e0e6ed;
padding: 12px 10px;
width: 181px;
}
.form-form .form-form-wrap form .social a.social-fb {
margin-right: 15px;
}
.form-form .form-form-wrap form .social a.social-fb svg {
color: #1b55e2;
}
.form-form .form-form-wrap form .social a.social-fb .brand-name {
color: #1b55e2;
}
@media (max-width: 991px) {
.form-form {
width: 100%;
}
.form-form .form-form-wrap {
min-width: auto;
margin: 0 auto;
}
.form-form .form-container .form-content {
border-radius: 15px;
}
}
@media (max-width: 575px) {
.form-form .form-form-wrap form .field-wrapper.toggle-pass {
margin-bottom: 28px;
}
.form-form .form-form-wrap form .social a.social-fb {
margin-right: auto;
margin-left: auto;
margin-bottom: 10px;
display: block;
}
.form-form .form-form-wrap form .social a.social-github {
margin-right: auto;
margin-left: auto;
margin-bottom: 10px;
display: block;
}
.form-form .form-container .form-content {
border: none;
border-radius: 0px;
}
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.form-form .form-form-wrap {
width: 100%;
}
.form-form .form-container {
height: 100%;
}
}
@supports (-ms-ime-align: auto) {
.form-form .form-form-wrap {
height: 100%;
}
}
.btn {
transition: all 0.3s ease-out;
-webkit-transition: all 0.3s ease-out;
}
.main-container {
color: #fff;
}
.main-container .background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: block;
}
.main-container .background path {
opacity: .1;
fill: #e5edff;
border-color: #e2e8f0;
}
.bg-100 {
background-image: url('../../../images/bk1.jpg');
background-repeat: no-repeat;
background-size: cover;
}
@@ -0,0 +1,282 @@
*, ::after, ::before {
box-sizing: border-box;
}
.wrapper {
width: 100%;
height:100%
}
body {
font-size: 14px;
color: #525f7f;
background-color: #f8f9fe;
letter-spacing: 0.5px;
overflow-x: hidden;
font-family: "Roboto",sans-serif;
}
.authentication-header {
position: absolute;
background: #165078;
top: 0px;
left: 0px;
right: 0px;
height: 50%;
}
.d-flex {
display: flex !important;
}
.justify-content-center {
justify-content: center !important;
}
.align-items-center {
align-items: center !important;
}
.section-authentication-sign-in {
height: 100vh;
}
.btn {
display: inline-block;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: .375rem .75rem;
font-size: 1rem;
border-radius: .25rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.btn-primary {
color: #fff;
background-color: #0d6efd;
border-color: #0d6efd;
}
.my-4 {
margin-top: 1.5rem !important;
margin-bottom: 1.5rem !important;
}
.btn {
letter-spacing: 0.5px;
}
.btn-primary {
color: #fff;
background-color: #8833ff;
border-color: #8833ff;
}
[type=button]:not(:disabled), [type=reset]:not(:disabled), [type=submit]:not(:disabled), button:not(:disabled) {
cursor: pointer;
}
.bx {
font-family: "boxicons" !important;
font-weight: normal;
font-style: normal;
font-variant: normal;
line-height: inherit;
display: inline-block;
text-transform: none;
speak: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.btn i {
vertical-align: middle;
font-size: 1.3rem;
margin-top: -1em;
margin-bottom: -1em;
margin-right: 5px;
}
.form-check {
display: block;
min-height: 1.5rem;
padding-left: 1.5em;
margin-bottom: 0.12rem;
}
.form-switch {
padding-left: 2.5em;
}
.form-check-input {
width: 1em;
height: 1em;
margin-top: 0.25em;
vertical-align: top;
background-color: #fff;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
border: 1px solid rgba(0,0,0,.25);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.form-check .form-check-input {
float: left;
margin-left: -1.5em;
}
[type=checkbox].form-check-input {
border-radius: .25em;
}
:checked.form-check-input {
background-color: #0d6efd;
border-color: #0d6efd;
}
.form-switch .form-check-input {
width: 2em;
margin-left: -2.5em;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");
background-position: left center;
border-radius: 2em;
transition: background-position .15s ease-in-out;
}
:checked.form-check-input {
background-color: #8833ff;
border-color: #8833ff;
}
:checked[type=checkbox].form-check-input {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
}
.form-switch :checked.form-check-input {
background-position: right center;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
}
label {
display: inline-block;
font-weight: 400 !important;
}
.form-label {
margin-bottom: 0.5rem;
}
.input-group {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.form-control {
display: block;
width: 100%;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.border-end-0 {
border-right: 0 !important;
}
.input-group > .form-control, .input-group > .form-select {
position: relative;
flex: 1 1 auto;
width: 1%;
min-width: 0px;
}
:not(.has-validation).input-group > :nth-last-child(n+3).dropdown-toggle, :not(.has-validation).input-group > :not(:last-child):not(.dropdown-menu):not(.dropdown-toggle) {
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.input-group-text {
display: flex;
align-items: center;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: center;
white-space: nowrap;
background-color: #e9ecef;
border: 1px solid #ced4da;
border-radius: .25rem;
}
.bg-transparent {
background-color: transparent !important;
}
.input-group > :not(:first-child):not(.invalid-feedback):not(.invalid-tooltip):not(.valid-feedback):not(.valid-tooltip):not(.dropdown-menu) {
margin-left: -1px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.login-separater span {
position: relative;
top: 26px;
margin-top: -10px;
background: #ffffff;
padding: 5px;
font-size: 12px;
color: #cbcbcb;
z-index: 1;
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
.shadow-sm {
box-shadow: 0px 0.12rem 0.25rem rgba(0,0,0,0.075) !important;
}
.btn-white {
background-color: #fff;
border-color: #e7eaf3;
}
img, svg {
vertical-align: middle;
}
.me-2 {
margin-right: 0.5rem !important;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
margin-top: 0px;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
.h3, h3 {
font-size: calc(1.3rem + 0.6vw);
}
/* @media all and (min-width:1200px) */
.h3, h3 {
font-size: 1.75rem;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
color: #32393f;
}
p {
margin-top: 0px;
margin-bottom: 1rem;
}
.card-body {
box-shadow: 1px -1px 20px 1px #403c4329;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

@@ -0,0 +1,441 @@
body {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Poppins",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji" !important;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
body {
font-family: Nunito,sans-serif;
font-size: 0.8rem;
font-weight: 400;
color: #3a3a3a;
background: #f8f8f8;
}
body {
min-height: calc(-150px + 100%);
position: relative;
padding-bottom: 90px;
}
body {
overflow-x: hidden !important;
}
.rounded {
border-radius: .25rem !important;
}
.rounded {
border-radius: 50px !important;
}
body.no-footer {
padding-bottom: initial;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
html {
width: 100%;
height: 100%;
background: #f8f8f8;
}
html {
width: 100%;
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
.bootstrap-tagsinput.focus, .form-control:focus {
border-color: rgba(237,113,23,.6);
}
.has-float-label label::after, .has-float-label>span::after {
background: #fff;
}
.has-float-label label::after, .has-float-label > span::after {
content: " ";
display: block;
position: absolute;
height: 5px;
top: 3px;
left: -0.2em;
right: -0.2em;
z-index: -1;
}
.bootstrap-tagsinput, .form-control {
border-radius: .1rem;
outline: initial!important;
box-shadow: initial!important;
font-size: .8rem;
padding: .5rem .75rem;
line-height: 1;
border: 1px solid #d7d7d7;
background: #fff;
color: #3a3a3a;
border-color: #d7d7d7;
height: calc(2em + .8rem);
}
.fixed-background {
background: url(./images/bk.jpg) no-repeat center center fixed;
background-size: cover;
width: 100%;
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
main.default-transition {
transition: margin-left .3s;
}
main {
margin: 0 !important;
height: 100%;
}
main .container {
height: 100%;
}
.background .mx-auto {
margin-left: auto !important;
margin-right: auto !important;
}
.card {
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
min-width: 0px;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,0.125);
border-radius: .25rem;
}
.card {
border: initial;
background: #fff;
border-radius: .1rem;
box-shadow: 0px 1px 15px rgba(0,0,0,0.04), 0px 1px 6px rgba(0,0,0,0.04);
}
.auth-card {
display: flex;
flex-direction: row;
box-shadow: 0px 1px 15px rgba(0,0,0,0.04), 0px 1px 6px rgba(0,0,0,0.04);
}
.rounded .card {
border-radius: .75rem;
}
.rounded .card.auth-card {
border-top-left-radius: 1.25rem;
border-bottom-left-radius: 1.25rem;
}
.position-relative {
position: relative !important;
}
.auth-card .image-side {
position: absolute;
width: 40%;
background: url(./images/aside_bk.png) no-repeat center top;
background-size: cover;
padding: 80px 40px;
}
.image-side::before {
content: '';
height: 100%;
width: 100%;
background: rgba(77, 76, 178, 0.9);
position: absolute;
left: 0;
z-index: 0;
top:0px
}
.text-layer {
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
top: 0px;
background-size: cover;
display: flex;
align-items: center;
flex-direction: column;
padding: 80px 40px
}
.rounded .auth-card .image-side {
border-top-left-radius: 0.75rem;
border-bottom-left-radius: 0.75rem;
}
.auth-card .form-side {
width: 60%;
padding: 80px;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
}
a {
color: #3a3a3a;
transition: color .2s;
outline: initial !important;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0px;
margin-bottom: 0.5rem;
}
p {
margin-top: 0px;
margin-bottom: 1rem;
}
p {
font-size: 0.85rem;
line-height: 1.3rem;
font-family: Nunito,sans-serif;
}
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h6, .h6 {
font-size: 1rem;
}
.text-white {
color: #fff !important;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
line-height: 1.3;
}
h6 {
font-size: 1rem;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.mb-4, .my-4 {
margin-bottom: 1.5rem !important;
}
.has-float-label {
display: block;
position: relative;
}
.d-flex {
display: -webkit-box !important;
display: -ms-flexbox !important;
display: flex !important;
}
.justify-content-between {
-webkit-box-pack: justify !important;
-ms-flex-pack: justify !important;
justify-content: space-between !important;
}
.align-items-center {
-webkit-box-align: center !important;
-ms-flex-align: center !important;
align-items: center !important;
}
button {
border-radius: 0;
}
input, button, select, optgroup, textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button, input {
overflow: visible;
}
button, select {
text-transform: none;
}
button {
color: #3a3a3a;
outline: initial !important;
}
button, [type='button'], [type='reset'], [type='submit'] {
-webkit-appearance: button;
}
button {
color: #3a3a3a;
outline: initial !important;
}
.btn {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: .25rem;
-webkit-transition: color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-lg, .btn-group-lg > .btn {
padding: .5rem 1rem;
font-size: 1.25rem;
line-height: 1.5;
border-radius: .3rem;
}
.btn {
border-radius: 50px;
outline: initial !important;
box-shadow: none !important;
box-shadow: initial !important;
font-size: 0.8rem;
padding: .5rem 1.25rem .5rem 1.25rem;
transition: background-color box-shadow .1s linear;
}
.btn-shadow {
box-shadow: 0px 1px 2px 0px rgba(0,0,0,0.15), 0px 1px 3px 1px rgba(0,0,0,0.15) !important;
transition: background-color box-shadow .1s linear;
}
.btn-group-lg > .btn, .btn-group-sm > .btn, .btn-lg {
border-radius: 50px;
}
.btn-primary {
background-color: #00365a;
border-color: #00365a;
color: #fff;
}
.btn-group-lg > .btn, .btn-lg {
line-height: 1.5;
font-weight: 700;
letter-spacing: 0.05rem;
padding: .75rem 2.6rem .6rem 2.6rem;
}
button:not(:disabled), [type='button']:not(:disabled), [type='reset']:not(:disabled), [type='submit']:not(:disabled) {
cursor: pointer;
}
.form-control {
display: block;
width: 100%;
height: calc(1.5em + 2px + 0.75rem);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
-webkit-transition: border-color 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out;
transition: border-color 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out;
transition: border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;
transition: border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out;
}
.bootstrap-tagsinput, .form-control {
border-radius: .1rem;
outline: initial !important;
box-shadow: initial !important;
font-size: 0.8rem;
padding: .5rem .75rem;
line-height: 1;
border: 1px solid #d7d7d7;
background: #fff;
color: #3a3a3a;
border-color: #d7d7d7;
height: calc(2em + 0.8rem);
}
.has-float-label label, .has-float-label > span, .has-top-label label, .has-top-label > span {
color: rgba(58, 58, 58, 0.7);
}
.has-float-label label, .has-float-label > span:last-of-type {
position: absolute;
cursor: text;
font-size: 90%;
opacity: 1;
top: -0.4em;
left: 0.75rem;
z-index: 3;
line-height: 1;
padding: 0 1px;
}
.logo-single {
width: 110px;
height: 35px;
background: url(../logos/black.svg) no-repeat;
background-position: center center;
display: inline-block;
margin-bottom: 60px;
}
h2, .h2 {
font-size: 2rem;
}
.text-white {
color: #fff !important;
}
.mb-0, .my-0 {
margin-bottom: 0px !important;
}
.white {
color: #fff !important;
}
@media all and (max-width:768px) {
.auth-card .form-side {
width: 100%;
padding: 40px;
}
.auth-card .image-side {
display: none;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
<svg id="Capa_1" enable-background="new 0 0 510 510" height="512" viewBox="0 0 510 510" width="512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="lg1"><stop offset="0" stop-color="#7faef4"/><stop offset="1" stop-color="#4c8df1"/></linearGradient><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="72.017" x2="448.659" xlink:href="#lg1" y1="30.771" y2="407.412"/><linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="393.324" x2="118.808" xlink:href="#lg1" y1="393.324" y2="118.808"/><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="220.253" x2="342.575" y1="220.253" y2="342.575"><stop offset="0" stop-color="#4166bf"/><stop offset="1" stop-color="#4256ac"/></linearGradient><linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="368.352" x2="227.221" y1="309.634" y2="168.502"><stop offset="0" stop-color="#4256ac" stop-opacity="0"/><stop offset="1" stop-color="#1b1464"/></linearGradient><linearGradient id="lg2"><stop offset="0" stop-color="#ebeff0"/><stop offset="1" stop-color="#e3e5e4"/></linearGradient><linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="51.502" x2="399.05" xlink:href="#lg2" y1="-7.217" y2="340.331"/><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="232.637" x2="276.114" xlink:href="#lg2" y1="303.885" y2="381.967"/><g><path d="m239.907 2.943c-38.555 15.625-127.875 53.362-171.842 62.404-9.715 1.998-16.648 10.61-16.648 20.528v127.182c0 99.733 44.909 194.164 122.273 257.105l23.878 19.426c33.451 27.215 81.411 27.215 114.863 0l23.878-19.426c77.364-62.941 122.273-157.376 122.273-257.109v-127.178c0-9.918-6.933-18.53-16.648-20.528-43.967-9.042-133.287-46.779-171.841-62.404-9.683-3.924-20.503-3.924-30.186 0z" fill="url(#SVGID_1_)"/><g><circle cx="255" cy="255" fill="url(#SVGID_2_)" r="169.321"/><path d="m405.238 255c0 45.321-20.067 85.954-51.801 113.5-26.365 22.886-60.783 22.738-98.437 22.738-37.275 0-71.379.426-97.639-22.05-32.195-27.555-52.599-68.489-52.599-114.188 0-82.974 67.264-150.238 150.238-150.238s150.238 67.264 150.238 150.238z" fill="url(#SVGID_3_)"/><path d="m255.332 127.937c-38.909 0-68.676 29.767-68.676 68.676 0 20.788 7.232 39.467 21.552 52.363l121.193 121.193c5.079-3.425 19.423 2.333 24.035-1.67 31.734-27.546 51.802-68.178 51.802-113.499 0-2.749-.079-5.48-.225-8.193l-97.318-97.318c-12.896-14.32-31.575-21.552-52.363-21.552z" fill="url(#SVGID_4_)"/><circle cx="255" cy="196.281" fill="url(#SVGID_5_)" r="70.452"/><path d="m348.476 315.4c19.076 12.239 20.97 39.524 3.686 54.185-.199.169-.398.337-.598.505-26.113 21.943-59.795 35.145-96.564 35.145-37.133 0-71.107-13.469-97.327-35.793-.102-.087-.204-.174-.306-.261-17.185-14.694-15.152-41.941 3.917-54.088 26.972-17.181 59.001-27.131 93.351-27.131 34.573 0 66.77 10.069 93.841 27.438z" fill="url(#SVGID_6_)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,4 @@
@mixin box-shadow($shadow...) {
box-shadow: $shadow;
}
@@ -0,0 +1,92 @@
@mixin button-variant( $background, $border: $background, $hover-background: darken($background, 5%), $hover-border: $hover-background, $hover-box-shadow: 0 4px 10px rgba($background, .25), $active-background: darken($background, 5%), $active-border: darken($border, 10%), $active-box-shadow: inset 0 3px 5px rgba($black, .125)) {
color: color-yiq($background);
border-color: $border;
// @include gradient-bg($background);
@include box-shadow($btn-box-shadow);
@include hover {
color: color-yiq($hover-background);
//@include gradient-bg($hover-background);
border-color: $hover-border;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05), $hover-box-shadow;
}
&:focus,
&.focus {
box-shadow: 0 0 0 3px rgba($background, .15), 0 3px 15px rgba($background, 0.2), 0 2px 5px rgba($black, .1);
}
// Disabled comes first so active can properly restyle
&.disabled,
&:disabled {
background-color: $background;
border-color: $border;
box-shadow: none;
cursor: not-allowed;
}
&:not(:disabled):not(.disabled):active,
&:not(:disabled):not(.disabled).active,
.show>&.dropdown-toggle {
color: color-yiq($active-background);
background-color: $active-background;
border-color: $active-border;
background-image: none;
//box-shadow: $active-box-shadow !important;
}
}
@mixin button-outline-variant( $color, $color-hover: color-yiq($color), $active-background: $color, $active-border: $color) {
$button-text-color: $color; // Fix light or white outlined buttons
@if ($color==theme-color("light") or $color==$white) {
$button-text-color: color-yiq($color);
}
background-color: transparent;
background-image: none;
border-color: $color;
color: $button-text-color;
&:hover {
color: color-yiq($color);
background-color: $active-background;
border-color: $active-border;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05), 0 4px 10px rgba($color, .25);
}
&:focus,
&.focus {
box-shadow: 0 0 0 3px rgba($color, .15), 0 3px 15px rgba($color, 0.2), 0 2px 5px rgba($black, .1) !important;
}
&.disabled,
&:disabled {
color: $color;
background-color: transparent;
box-shadow: none;
}
&:not(:disabled):not(.disabled):active,
&:not(:disabled):not(.disabled).active,
.show>&.dropdown-toggle {
color: color-yiq($active-background);
background-color: $active-background;
border-color: $active-border;
// &:focus {
// // Avoid using mixin so we can pass custom focus shadow properly
// // box-shadow: inset 0 3px 5px rgba($black, .125) !important;
// }
}
}
// Button sizes
@mixin button-size( $padding-y, $padding-x, $font-size, $line-height, $border-radius) {
padding: $padding-y $padding-x;
font-size: $font-size;
line-height: $line-height;
border-radius: $border-radius;
}
@@ -0,0 +1,7 @@
@mixin clearfix() {
&::after {
display: block;
clear: both;
content: "";
}
}
@@ -0,0 +1,74 @@
@mixin awesome-flex {
display: -webkit-box;
display: -webkit-flex;
display: -moz-box;
display: -ms-flexbox;
display: flex;
}
@mixin awesome-align-items-center {
-webkit-box-align: center;
-webkit-align-items: center;
-moz-box-align: center;
-ms-flex-align: center;
align-items: center;
}
@mixin awesome-justify-content-center {
-webkit-box-pack: center;
-webkit-justify-content: center;
-moz-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
}
@mixin awesome-justify-content-between {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-moz-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
@mixin awesome-flex-row {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-direction: row;
-moz-box-orient: horizontal;
-moz-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
}
@mixin awesome-flex-column {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-moz-box-orient: vertical;
-moz-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
@mixin gradient-bg($color) {
background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x;
}
@mixin box-shadow($shadow...) {
box-shadow: $shadow;
}
@mixin border-radius($radius: $border-radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
border-radius: $radius;
}
// @mixin awesome-trainsition-all ($seconds) {
// -webkit-transition: all $seconds;
// -moz-transition: all $seconds;
// -ms-transition: all $seconds;
// -o-transition: all $seconds;
// transition: all $seconds;
// }
@@ -0,0 +1,10 @@
@mixin float-left {
float: left !important;
}
@mixin float-right {
float: right !important;
}
@mixin float-none {
float: none !important;
}
@@ -0,0 +1,41 @@
// Gradients
@mixin gradient-bg($color) {
background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x;
}
// Horizontal gradient, from left to right
//
// Creates two color stops, start and end, by specifying a color and position for each color stop.
@mixin gradient-x($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) {
background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent);
background-repeat: repeat-x;
}
// Vertical gradient, from top to bottom
//
// Creates two color stops, start and end, by specifying a color and position for each color stop.
@mixin gradient-y($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) {
background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent);
background-repeat: repeat-x;
}
@mixin gradient-directional($start-color: #555, $end-color: #333, $deg: 45deg) {
background-image: linear-gradient($deg, $start-color, $end-color);
background-repeat: repeat-x;
}
@mixin gradient-x-three-colors($start-color: #00b3ee, $mid-color: #7a43b6, $color-stop: 50%, $end-color: #c3325f) {
background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);
background-repeat: no-repeat;
}
@mixin gradient-y-three-colors($start-color: #00b3ee, $mid-color: #7a43b6, $color-stop: 50%, $end-color: #c3325f) {
background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);
background-repeat: no-repeat;
}
@mixin gradient-radial($inner-color: #555, $outer-color: #333) {
background-image: radial-gradient(circle, $inner-color, $outer-color);
background-repeat: no-repeat;
}
@mixin gradient-striped($color: rgba(255,255,255,.15), $angle: 45deg) {
background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);
}
@@ -0,0 +1,26 @@
@mixin hover {
&:hover { @content; }
}
@mixin hover-focus {
&:hover,
&:focus {
@content;
}
}
@mixin plain-hover-focus {
&,
&:hover,
&:focus {
@content;
}
}
@mixin hover-focus-active {
&:hover,
&:focus,
&:active {
@content;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

@@ -0,0 +1,81 @@
<div class="container">
<div class="oe_styling_v8">
<div class="overflow-hidden mb-5 my-3 p-4 deep-3"
style="background-color:#fbfcfd; border-radius:20px; border: 1px solid #d2caca66;">
<div class="container">
<div class="row">
<p style="width: 100%; font-size: 18px; text-indent:1em;" class="px-sm-4 p-2">
This module is base of our themes, it contains all the common features of our themes.
</p>
</div>
</div>
</div>
<h2 class="w-100 text-center mb-3">(Current Realse V16.0.0.2 -- 2023.1.5)</h2>
<div class="shadow row my-5 p-3 pb-2 overflow-hidden"
style="background-color:#fbfcfd; border-radius: 5px; border: 1px solid #d2caca66;">
<img src="./banner.png" style="width:100%; " />
</div>
<h2>
Our Other Products
</h2>
<div class="shadow row my-5 p-3 pb-2 overflow-hidden"
style="background-color:#fbfcfd; border-radius: 5px; border: 1px solid #d2caca66;">
<div class="container">
<h3 style="opacity:1; padding:10px 0px; color:#201111; letter-spacing:0.2em; text-transform:uppercase">
<b>Awesome theme Pro</b>
</h3>
<hr class="mb-4 mt-0">
<a href="https://apps.odoo.com/apps/themes/15.0/awesome_theme_pro/">
<img src="./awesome_theme.gif" class="img img-fluid mt-3" loading="lazy">
</a>
</div>
</div>
<div class="shadow row my-5 p-3 pb-2 overflow-hidden"
style="background-color:#fbfcfd; border-radius: 5px; border: 1px solid #d2caca66;">
<div class="container">
<h3 style="opacity:1; padding:10px 0px; color:#201111; letter-spacing:0.2em; text-transform:uppercase">
<b>Anita Modeler</b>
</h3>
<hr class="mb-4 mt-0">
<a href="https://apps.odoo.com/apps/modules/14.0/anita_modeler/">
<img src="./anita_modeler.png" class="img img-fluid mt-3" loading="lazy">
</a>
</div>
</div>
<div class="shadow row my-5 p-3 pb-2 overflow-hidden"
style="background-color:#fbfcfd; border-radius: 5px; border: 1px solid #d2caca66;">
<div class="container">
<h3 style="opacity:1; padding:10px 0px; color:#201111; letter-spacing:0.2em; text-transform:uppercase">
<b>Anita List</b>
</h3>
<hr class="mb-4 mt-0">
<a href="https://apps.odoo.com/apps/modules/15.0/anita_list/">
<img src="./anita_list.gif" class="img img-fluid mt-3" loading="lazy">
</a>
</div>
</div>
<div class="shadow row my-5 p-3 pb-2 overflow-hidden"
style="background-color:#fbfcfd; border-radius: 5px; border: 1px solid #d2caca66;">
<div class="container">
<h3 style="opacity:1; padding:10px 0px; color:#201111; letter-spacing:0.2em; text-transform:uppercase">
<b>Admin Theme</b>
</h3>
<hr class="mb-4 mt-0">
<a href="https://apps.odoo.com/apps/modules/15.0/anita_list/">
<img src="./admin_theme.gif" class="img img-fluid mt-3" loading="lazy">
</a>
</div>
</div>
</div>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

@@ -0,0 +1,36 @@
/** @odoo-module **/
import { Component, useState, useExternalListener} from "@odoo/owl";
import { registry } from "@web/core/registry";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useService } from "@web/core/utils/hooks";
export class ThemeCustomizer extends Component {
setup() {
super.setup();
this.actionService = useService("action");
}
_on_toggle_click() {
// url action
this.actionService.doAction({
type: "ir.actions.act_url",
url: "https://apps.odoo.com/apps/themes/15.0/anita_theme_setting/",
target: "current",
});
}
}
ThemeCustomizer.template = "anita_theme_base.customizer";
ThemeCustomizer.components = {
Dropdown: Dropdown,
DropdownItem: DropdownItem,
};
export const systrayItem = {
Component: ThemeCustomizer,
};
registry.category("systray").add("anita_theme_base.customizer", systrayItem, { sequence: 1 });
@@ -0,0 +1,372 @@
.customizer {
width: 395px;
right: -500px;
padding: 0;
background-color: #FFF;
z-index: 1001;
position: fixed;
top: 0;
bottom: 0;
height: 100vh;
-webkit-transition: right .4s cubic-bezier(.05, .74, .2, .99);
transition: right .4s cubic-bezier(.05, .74, .2, .99);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
border-left: 1px solid rgba(0, 0, 0, .05);
box-shadow: 0 15px 30px 0 rgba(0, 0, 0, .11), 0 5px 15px 0 rgba(0, 0, 0, .08);
color: #212529;
a, label, span, i {
color: #212529;
}
i {
&:hover {
color: #71639e !important;
}
}
@include media-breakpoint-down(sm) {
right: -550px;
width: 100%;
}
&.show {
right: 0
}
.dropdown {
height: 100% !important;
.dropdown-toggle {
height: 100% !important;
}
}
svg {
&:hover {
path {
fill: #71639e;
}
}
}
.customizer-content {
position: relative;
ul.navbar-nav {
li.nav-item {
&:focus {
outline: none;
}
a.nav-link {
padding: 5px;
}
}
}
.theme_mode_tool_bar {
height: 30px;
line-height: 30px;
border-top: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
background-color: #eee;
text-align: center;
margin-top: -1px;
.create_new_theme_mode {
font-size: 1.1rem;
}
}
.add_new_style_icon {
fill: #dee2e6;
}
.operation-toolbar {
height: 30px;
background-color: #f2f2f2;
border-bottom: 1px solid #dee2e6;
.tool-bar-icon {
margin: 0px 5px;
}
}
.mode_tools {
position: absolute;
right: 1rem;
}
.style-preview-box {
position: relative;
width: 100px;
height: 60px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
overflow: hidden;
background-size: cover;
&:hover {
transform: translate3d(0px, 0px, -2px);
}
.preview-sidebar-box {
width: 30px;
border-right: 1px solid #dee2e6;
}
.preview-main-box {
.admin_header-box {
height: 30px
}
.footer-box {
height: 30px;
}
}
.preview-header {
border-bottom: 1px solid #ebebeb;
}
.preview-footer {
border-top: 1px solid #ebebeb;
}
.nav-pills {
.nav-link.active, .nav-pills .show {
> .nav-link {
background-color: transparent !important;
border: 1px solid #e3e3e3 !important
}
}
}
}
.theme-style-contianer {
.theme_style_tab {
padding: 5px;
box-sizing: border-box;
background: transparent;
border: 1px solid;
border-color: transparent;
margin: 1px;
&:hover {
border-color: #5A8DEE !important;
.operation-toolbar {
top: 0px;
}
}
&.active {
background: transparent;
border: 1px solid darken($color: #5A8DEE, $amount: 10) !important;
}
}
.theme_style_tool_bar {
.theme_style_tab {
.tool_bar_item {
font-size: 16px;
&:hover {
color: black;
border-color: #5A8DEE !important;
}
}
}
}
}
.style_tool_bar {
background-color: #eee;
height: 30px;
}
.style_item_container {
.style_item_group {
.style_items {
.style_item {
margin-left: 10px;
width: 220px;
.title {
height: 30px;
line-height: 30px;
display: flex;
}
.color-container {
.style_color_item {
margin-left: 5px;
}
.pickr {
margin: 5px;
.pcr-button:before {
background: transparent !important;
}
}
.color_value {
display: flex;
align-items: center;
justify-items: left;
}
}
.colorpicker-input-addon {
border: 1px solid #dee2e6;
}
}
}
.style_item_group_name {
height: 30px;
line-height: 30px;
border-top: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
background-color: #eee;
text-align: center;
margin-top: -1px;
.style_group_dropdown {
&.dropdown-toggle {
font-size: 0.75rem;
&::after {
display: none !important;
}
}
}
}
.style_sub_group_name {
color: #182139;
background: rgba(238,238,238,0.51);
height: 30px;
line-height: 30px;
padding-left: 10px;
border-bottom: 1px solid #dee2e6;
.sub_group_toggler i {
font-size: 0.75rem;
}
}
.sub-group-body {
.style_group_dropdown {
&.dropdown-toggle {
font-size: 0.75rem;
&::after {
display: none !important;
}
}
}
.sub_group_panel {
background: #f6f6f6;
}
}
.add_style_item {
float: right;
}
.add_item_group_container {
margin-bottom: -1.5rem;
border-bottom: none;
}
}
.add_new_group {
height: 30px;
line-height: 30px;
border-top: none;
border-bottom: 1px solid #dee2e6;
background-color: #eee;
text-align: center;
}
}
}
.customizer-close {
position: absolute;
right: 10px;
top: 10px;
padding: 7px;
width: auto;
z-index: 10;
color: #727E8C;
i {
font-size: 1.3rem
}
.customizer-toggle {
background: #5A8DEE;
color: #FFF;
display: block;
box-shadow: -3px 0 8px rgba(0, 0, 0, .1);
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
position: absolute;
top: 50%;
width: 38px;
height: 38px;
left: -39px;
text-align: center;
line-height: 44px;
cursor: pointer
}
.color-box {
height: 30px;
width: 30px;
margin: .5rem;
border-radius: .3rem;
cursor: pointer;
&.selected {
box-shadow: 0 0 0 3px rgba(52, 144, 220, .5)
}
}
ul.list-inline li {
display: inline-block;
}
h5 {
margin-bottom: .5rem;
font-family: Rubik, Helvetica, Arial, serif;
font-weight: 400;
line-height: 1.2;
color: #475F7B;
}
.custom-control-label {
padding-top: 2px;
}
}
.top_title {
height: 60px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-bottom: 1px solid #ebebeb;
box-shadow: -1px 2px 25px 0 rgba(0, 0, 0, 0.1);
}
.customizer-footer {
align-self: flex-end;
flex-shrink: 0;
border-top: 1px solid rgb(235, 235, 235);
background-color: rgba(255, 255, 255, 0.733);
box-shadow: 0 -4px 25px 0 rgba(0,0,0,.1);
height: 50px;
width: 100%;
button {
margin-left: 0.5rem;
}
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<!-- style setting -->
<div t-name="anita_theme_base.customizer" class="dropdown" owl="1">
<a role="button" href="#" class="customizer-toggle dropdown-toggle o-no-caret o-dropdown--narrow" t-on-click="_on_toggle_click">
<span class="fa fa-cog fa-fw"/>
</a>
</t>
</templates>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
+96
View File
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
{
'name': "anita_theme_setting",
'summary': """
anita theme
advance theme
sunsine theme
mnk theme for odoo
free backend theme
odoo theme
owl theme
multi mode theme
free theme
multi style theme
button style theme
crazy theme
nice theme
mutli tab theme
multi layout theme
mult tab style theme
""",
'description': """
anita theme setting for odoo, it is a powerfull theme
""",
'author': "OpenERPnext",
'website': "https://www.openerpnext.com",
'live_test_url': 'https://www.openerpnext.com',
'images': [
'static/description/screen_shot.png',
'static/description/banner.png',
],
'license': 'OPL-1',
'category': 'Themes/Backend',
'version': '17.0.0.3',
'price': 999,
'depends': ['web', 'web_enterprise'],
'data': [
'security/ir.model.access.csv',
# settings, setting page add by modules
'views/anita_theme_mode.xml',
'views/anita_theme_style.xml',
'views/anita_theme_style_group.xml',
'views/anita_theme_style_item.xml',
'views/anita_theme_style_item_sub_group.xml',
'views/anita_theme_var.xml',
'views/anita_import_theme_style.xml',
'views/anita_web.xml',
'views/anita_bk_image_wizard.xml',
'views/anita_res_setting.xml',
],
'assets': {
'web.assets_backend': [
# aniate.css
'anita_theme_setting/static/libs/animate.css',
# theme customizer
'anita_theme_setting/static/src/components/customizer/*',
'anita_theme_setting/static/src/components/settings/*.js',
'anita_theme_setting/static/src/components/settings/*.xml',
'anita_theme_setting/static/src/anita_theme_setting.js',
# according
'anita_theme_setting/static/src/components/accordion/*',
# dialog
'anita_theme_setting/static/src/components/dialog/*',
# color picker
'anita_theme_setting/static/src/components/color_picker/color_picker.js',
'anita_theme_setting/static/src/components/color_picker/color_picker.xml',
'anita_theme_setting/static/src/components/color_picker/color_picker.scss',
'anita_theme_setting/static/libs/spectrum.js',
'anita_theme_setting/static/libs/spectrum.css',
'anita_theme_setting/static/src/anita_icon.js',
# client
'anita_theme_setting/static/src/components/webclient/webclient.js',
],
'web.assets_web': [
('replace', 'web_enterprise/static/src/main.js', 'anita_theme_setting/static/src/anita_main.js'),
]
}
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import anita_home
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
import odoo
import odoo.modules.registry
from odoo.tools.translate import _
from odoo.exceptions import AccessError
from odoo.addons.web.controllers.home import Home, SIGN_UP_REQUEST_PARAMS
from odoo.addons.web.controllers.utils import ensure_db, is_user_internal
from odoo import http
from odoo.http import request
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.misc import file_path
try:
from werkzeug.utils import send_file
except ImportError:
from odoo.tools._vendor.send_file import send_file
import json
import base64
import functools
import io
import os
import logging
_logger = logging.getLogger(__name__)
BASE_PATH = os.path.dirname(os.path.dirname(__file__))
class anitaHome(Home):
'''
inherit home to extend web.login style
'''
@http.route([
'/web/binary/company_logo',
'/logo',
'/logo.png',
], type='http', auth="none", cors="*")
def company_logo(self, dbname=None, **kw):
imgname = 'logo'
imgext = '.png'
dbname = request.db
uid = (request.session.uid if dbname else None) or odoo.SUPERUSER_ID
if not dbname:
response = http.Stream.from_path(file_path('web/static/img/logo.png')).get_response()
else:
try:
# create an empty registry
registry = odoo.modules.registry.Registry(dbname)
with registry.cursor() as cr:
company = int(kw['company']) if kw and kw.get('company') else False
if company:
cr.execute("""SELECT logo_web, write_date
FROM res_company
WHERE id = %s
""", (company,))
else:
cr.execute("""SELECT c.logo_web, c.write_date
FROM res_users u
LEFT JOIN res_company c
ON c.id = u.company_id
WHERE u.id = %s
""", (uid,))
row = cr.fetchone()
if row and row[0]:
image_base64 = base64.b64decode(row[0])
image_data = io.BytesIO(image_base64)
mimetype = guess_mimetype(image_base64, default='image/png')
imgext = '.' + mimetype.split('/')[1]
if imgext == '.svg+xml':
imgext = '.svg'
response = send_file(
image_data,
request.httprequest.environ,
download_name=imgname + imgext,
mimetype=mimetype,
last_modified=row[1],
response_class=Response,
)
else:
response = http.Stream.from_path(file_path('web/static/img/nologo.png')).get_response()
except Exception:
response = http.Stream.from_path(file_path(f'web/static/img/{imgname}{imgext}')).get_response()
return response
@@ -0,0 +1,12 @@
<odoo>
<data noupdate="0">
<function model="anita_theme_setting.mode_template" name="install_template">
<value eval="'template1'"/>
<value eval="'/anita_theme_setting/static/template/template1.scss'"/>
<value eval="['/anita_theme_setting/static/json/white_mode.json', '/anita_theme_setting/static/json/dark_mode.json']"/>
</function>
</data>
</odoo>
+21
View File
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from . import anita_setting_base
from . import anita_template_var_vals
from . import anita_theme_mode
from . import anita_theme_style
from . import anita_theme_style_item_group
from . import anita_theme_style_item
from . import anita_company
from . import anita_theme_users
from . import anita_theme_vars
from . import anita_setting_manager
from . import anita_res_setting
from . import anita_default_mode_data
from . import anita_mode_template
from . import anita_theme_style_item_sub_group
from . import anita_import_theme_style
from . import anita_default_menu_icon
from . import anita_menu_extend
from . import anita_favorite_menu
from . import anita_bk_image_wizard
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class AnitaBkImageWizard(models.TransientModel):
'''
background image wizard
'''
_name = 'anita_theme_setting.background_image_wizard'
_description = 'background image wizard'
data = fields.Binary(string='data', required=True)
opacity = fields.Float(string='theme opacity', default=0.8)
+130
View File
@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, exceptions
import base64
from datetime import date, datetime
from odoo.tools.misc import file_path
class AnitaCompany(models.Model):
'''
extend to support current mode and current style
'''
_inherit = "res.company"
_name = 'res.company'
setting_id = fields.Many2one(
comodel_name="anita_theme_setting.setting_base",
ondelete="cascade",
string="setting id")
def _get_default_favicon_icon(self):
"""
get default favicon icon form static path
"""
tmp_path = file_path('anita_theme_setting/static/src/img/favicon.png')
return base64.b64encode(open(tmp_path, 'rb').read())
favicon_icon = fields.Binary(
string='Web Favicon Icon', default='_get_default_favicon_icon')
def _get_default_small_logo(self):
'''
get the default small logo
:return:
'''
tmp_path = file_path('anita_theme_setting/static/src/img/res_small_logo.png')
return base64.b64encode(open(tmp_path, 'rb').read())
logo_small = fields.Binary(
default=_get_default_small_logo, string="Company Small Logo")
def get_mode_data(self):
'''
get mode data
:return:
'''
owner = 'res.company, {company_id}'.format(
company_id=self.env.user.company_id)
self.env["anita_theme_setting.theme_mode"].check_default_mode_data(owner)
return self.env["anita_theme_setting.theme_mode"].get_mode_data()
def get_mode_domain(self):
"""
get mode domain dynamic
:return:
"""
owner = 'res.company, {company_id}'.format(
company_id=self.env.user.company_id.id)
return [('owner', '=', owner)]
def get_company_mode_data(self):
'''
get company mode data
:return:
'''
return self.get_mode_data()
def ensure_setting_id(self):
'''
check setting id
:return:
'''
self.ensure_one()
if self.setting_id:
return
# copy use the global default data
values = self.env['res.config.settings'].get_theme_settings()
# get fields of anita_theme_setting.setting_base
field_names = [name for name, field in self.setting_id._fields.items() if name not in [
'id', 'create_uid', 'create_date', 'write_uid', 'write_date']]
# remove name not in fields
values = {k: v for k, v in values.items() if k in field_names}
del values["current_theme_mode"]
del values["current_theme_style"]
if not self.setting_id:
tmp_record = self.env["anita_theme_setting.setting_base"].create(values)
self.setting_id = tmp_record.id
return self.setting_id
def get_theme_setting(self):
'''
get company setting
:return:
'''
record = self.env.user.company_id
record.ensure_setting_id()
result = record.setting_id.read()[0]
result["current_theme_mode"] = record.setting_id.current_theme_mode.id
result["current_theme_style"] = record.setting_id.current_theme_style.id
for key, item in result.items():
if isinstance(item, datetime):
result[key] = fields.Datetime.to_string(item)
if isinstance(item, date):
result[key] = fields.Date.to_string(item)
return result
def check_default_data(self):
"""
check default data
:return:
"""
# check mode data
owner = 'res.company, {company_id}'.format(company_id=self.id)
self.env["anita_theme_setting.theme_mode"].check_default_mode_data(owner)
def save_theme_setting(self, settings):
'''
save theme setting
:param settings:
:return:
'''
self.ensure_setting_id()
self.setting_id.write(settings)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions
from odoo.tools import ormcache
import json
from odoo.tools.misc import file_path
import logging
_logger = logging.getLogger(__name__)
class AnitaDefaultMode(models.Model):
'''
default mode
'''
_name = 'anita_theme_setting.default_mode_data'
_description = 'anita Theme Setting Mode Template'
name = fields.Char(string="Name", required=True)
data = fields.Text(string="Content", required=True)
template_var_vals = fields.Text(string="Template Var Vals")
version = fields.Char(string="Version", required=True)
config_path = fields.Char(string="Config Path", help="This is the file path of the template")
_sql_constraints = [('name_unique', 'UNIQUE(name)', "Name Must Be Unique!")]
@ormcache('self.id')
def get_mode_data(self):
"""
get mode
:return:
"""
self.ensure_one()
mode_data = json.loads(self.data)
mode_data.update({
"name": self.name,
"version": self.version
})
return mode_data
@ormcache('self.id')
def get_var_val_cache(self):
"""
get var value cache
"""
self.ensure_one()
try:
var_vals = self.template_var_vals
var_vals = var_vals.replace('\'', '\"')
data = json.loads(var_vals)
except Exception as e:
_logger.error(e)
data = {}
var_val_cache = {}
for group_name in data:
group = data[group_name]
for var_item in group:
var_val_cache[var_item['name']] = var_item['value']
return var_val_cache
@ormcache()
def get_default_modes(self):
"""
get default modes
:return:
"""
records = self.search([])
default_models = []
for record in records:
default_models.append(record.get_mode_data())
return default_models
@api.model
def refresh_default_mode(self):
"""
udpate data
"""
records = self.search([])
for record in records:
if not record.config_path:
return
tmp_path = file_path(record.config_path.lstrip('/'))
if not tmp_path:
continue
with open(tmp_path, 'r') as fd:
mode_text = fd.read()
# check the data is valid json
try:
mode_data = json.loads(mode_text)
record.write({
"data": json.dumps(mode_data),
"template_var_vals": json.dumps(mode_data.get('template_var_vals', {}))
})
except Exception as e:
self.data = False
_logger.error(e)
@api.model
def install_mode(self, config_path, template):
"""
install mode
:return:
"""
# trim start '/'
tmp_path = file_path(config_path.lstrip('/'))
if not tmp_path:
raise exceptions.ValidationError('File not found! {}'.format(config_path))
with open(tmp_path, 'r') as fd:
mode_text = fd.read()
# check the data is valid json
try:
mode_data = json.loads(mode_text)
except Exception as e:
raise exceptions.ValidationError(
'Template {template} must be a valid json file!'.format(template=config_path))
name = mode_data.get('name')
if not name:
raise exceptions.ValidationError(
'Template {template} must has a name!'.format(template=config_path))
model_record = self.search([('name', '=', name)])
if not model_record:
self.create([{
"name": name,
"data": json.dumps(mode_data),
"version": mode_data.get('version', '1.0'),
"config_path": config_path,
"template_var_vals": mode_data.get("template_var_vals", "")
}])
else:
model_record.write({
"name": name,
"data": json.dumps(mode_data),
"version": mode_data.get('version', '1.0'),
"config_path": config_path,
"template_var_vals": mode_data.get("template_var_vals", "")
})
return model_record
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class AnitaFavoriteMenu(models.Model):
'''
user favorite menu
'''
_name = 'anita_theme_setting.favorite_menu'
_order = "sequence"
_description = "user favorite menu"
def _default_sequence(self):
return self.env['ir.sequence'].next_by_code('anita_theme_setting.favorite.menu')
sequence = fields.Integer(
string="sequence",
default=_default_sequence)
menu_id = fields.Many2one(
comodel_name='ir.ui.menu',
string='Favorite Menu',
required=True,
ondelete='cascade')
user_id = fields.Many2one(
comodel_name='res.users',
string='User Name',
default=lambda self: self.env.user.id)
@api.model
def unlink_menu_id(self, user_id, menu_id):
'''
unlink
:param user_id:
:param menu_id:
:return:
'''
value = self.search(
[('menu_id', '=', menu_id), ('user_id', '=', user_id)])
return value.unlink()
@api.model
def update_favorite_menu(self, favorites):
'''
update favoirte
:return:
'''
records = self.search([('user_id', '=', self.env.uid)])
records.unlink()
self.create(favorites)
return self.search_read([('user_id', '=', self.env.uid)])
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class AnitaImportThemeStyle(models.TransientModel):
'''
user theme style setting
'''
_name = 'anita_theme_setting.import_theme_style'
_description = 'user setting'
file = fields.Binary(string="Theme File", required=True)
@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
from odoo.tools.convert import xml_import, nodeattr2bool
import odoo
from odoo import api, fields, models, tools
from odoo.osv import expression
from .anita_default_menu_icon import svg_icons, font_icons
class AnitaMenuExtend(models.Model):
'''
extend to support font icon and svg icon
'''
_inherit = 'ir.ui.menu'
font_icon = fields.Char(string='font icon')
svg_icon = fields.Text(string='svg icon')
def load_web_menus(self, debug):
""" Loads all menu items (all applications and their sub-menus) and
processes them to be used by the webclient. Mainly, it associates with
each application (top level menu) the action of its first child menu
that is associated with an action (recursively), i.e. with the action
to execute when the opening the app.
:return: the menus (including the images in Base64)
"""
theme_setting = self.env["anita_theme_setting.setting_manager"].sudo().get_theme_setting()
icon_policy = theme_setting.get("icon_policy", "svg_icon")
menus = self.load_menus(debug)
favorite_cache = {}
if theme_setting.get("favorite_mode", False):
favorite_apps = self.env['anita_theme_setting.favorite_menu'].sudo().search(
[('user_id', '=', self.env.uid)])
favorite_cache = {fav.menu_id.id: fav for fav in favorite_apps}
web_menus = {}
for menu in menus.values():
if not menu['id']:
# special root menu case
web_menus['root'] = {
"id": 'root',
"name": menu['name'],
"children": menu['children'],
"appID": False,
"xmlid": "",
"actionID": False,
"actionModel": False,
"webIcon": None,
"webIconData": None,
"backgroundImage": menu.get('backgroundImage'),
"icon_policy": icon_policy,
}
else:
action = menu['action']
if menu['id'] == menu['app_id']:
# if it's an app take action of first (sub)child having one defined
child = menu
while child and not action:
action = child['action']
child = menus[child['children'][0]] if child['children'] else False
action_model, action_id = action.split(',') if action else (False, False)
action_id = int(action_id) if action_id else False
web_menus[menu['id']] = {
"id": menu['id'],
"name": menu['name'],
"children": menu['children'],
"appID": menu['app_id'],
"xmlid": menu['xmlid'],
"actionID": action_id,
"actionModel": action_model,
"svg_icon": menu.get('svg_icon'),
"font_icon": menu.get('font_icon'),
"webIcon": menu['web_icon'],
"webIconData": menu['web_icon_data'],
"is_favorite": True if favorite_cache.get(menu['id']) else False,
"sequence": favorite_cache.get(menu['id'], {}).sequence if favorite_cache.get(menu['id']) else 0
}
return web_menus
@api.model
# @tools.ormcache_context('self._uid', 'debug', keys=('lang',))
def load_menus(self, debug):
""" Loads all menu items (all applications and their sub-menus).
:return: the menu root
:rtype: dict('children': menu_nodes)
"""
theme_setting = self.env["anita_theme_setting.setting_manager"].sudo().get_theme_setting()
icon_policy = theme_setting.get("icon_policy", "svg_icon")
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon']
if icon_policy == "font_icon":
fields.append('font_icon')
elif icon_policy == "svg_icon":
fields.append('svg_icon')
else:
fields.append('web_icon')
fields.append('web_icon_data')
menu_roots = self.get_user_roots()
menu_roots_data = menu_roots.read(fields) if menu_roots else []
menu_root = {
'id': False,
'name': 'root',
'parent_id': [-1, ''],
'children': [menu['id'] for menu in menu_roots_data],
}
all_menus = {'root': menu_root}
if not menu_roots_data:
return all_menus
# menus are loaded fully unlike a regular tree view, cause there are a
# limited number of items (752 when all 6.1 addons are installed)
menus_domain = [('id', 'child_of', menu_roots.ids)]
blacklisted_menu_ids = self._load_menus_blacklist()
if blacklisted_menu_ids:
menus_domain = expression.AND([menus_domain, [('id', 'not in', blacklisted_menu_ids)]])
menus = self.search(menus_domain)
menu_items = menus.read(fields)
xmlids = (menu_roots + menus)._get_menuitems_xmlids()
# add roots at the end of the sequence, so that they will overwrite
# equivalent menu items from full menu read when put into id:item
# mapping, resulting in children being correctly set on the roots.
menu_items.extend(menu_roots_data)
# set children ids and xmlids
menu_items_map = {menu_item["id"]: menu_item for menu_item in menu_items}
for menu_item in menu_items:
menu_item.setdefault('children', [])
# set the default icons
if icon_policy == "svg_icon":
if not menu_item.get('svg_icon'):
menu_item["svg_icon"] = svg_icons.get(menu_item["name"], False) or False
elif icon_policy == "font_icon":
if not menu_item.get('font_icon'):
menu_item["font_icon"] = font_icons.get(menu_item["name"], False) or "fa fa-circle-o"
parent = menu_item['parent_id'] and menu_item['parent_id'][0]
menu_item['xmlid'] = xmlids.get(menu_item['id'], "")
if parent in menu_items_map:
menu_items_map[parent].setdefault(
'children', []).append(menu_item['id'])
all_menus.update(menu_items_map)
# sort by sequence
for menu_id in all_menus:
all_menus[menu_id]['children'].sort(key=lambda id: all_menus[id]['sequence'])
# recursively set app ids to related children
def _set_app_id(app_id, menu):
menu['app_id'] = app_id
for child_id in menu['children']:
_set_app_id(app_id, all_menus[child_id])
for app in menu_roots_data:
app_id = app['id']
_set_app_id(app_id, all_menus[app_id])
# filter out menus not related to an app (+ keep root menu)
all_menus = {menu['id']: menu for menu in all_menus.values() if menu.get('app_id')}
all_menus['root'] = menu_root
return all_menus
def _tag_menuitem_extend(self, rec, parent=None):
"""
extend tag menu item
"""
rec_id = rec.attrib["id"]
self._test_xml_id(rec_id)
# The parent attribute was specified, if non-empty determine its ID, otherwise
# explicitly make a top-level menu
values = {
'parent_id': False,
'active': nodeattr2bool(rec, 'active', default=True),
}
if rec.get('sequence'):
values['sequence'] = int(rec.get('sequence'))
if parent is not None:
values['parent_id'] = parent
elif rec.get('parent'):
values['parent_id'] = self.id_get(rec.attrib['parent'])
elif rec.get('web_icon'):
values['web_icon'] = rec.attrib['web_icon']
if rec.get('name'):
values['name'] = rec.attrib['name']
if rec.get('action'):
a_action = rec.attrib['action']
if '.' not in a_action:
a_action = '%s.%s' % (self.module, a_action)
act = self.env.ref(a_action).sudo()
values['action'] = "%s,%d" % (act.type, act.id)
if not values.get('name') and act.type.endswith(('act_window', 'wizard', 'url', 'client', 'server')) and act.name:
values['name'] = act.name
if not values.get('name'):
values['name'] = rec_id or '?'
groups = []
for group in rec.get('groups', '').split(','):
if group.startswith('-'):
group_id = self.id_get(group[1:])
groups.append(odoo.Command.unlink(group_id))
elif group:
group_id = self.id_get(group)
groups.append(odoo.Command.link(group_id))
if groups:
values['groups_id'] = groups
# read font icon
if rec.get('font_icon'):
values['font_icon'] = rec.get('font_icon')
if rec.get('svg_icon'):
values['svg_icon'] = rec.get('svg_icon')
data = {
'xml_id': self.make_xml_id(rec_id),
'values': values,
'noupdate': self.noupdate,
}
menu = self.env['ir.ui.menu']._load_records([data], self.mode == 'update')
for child in rec.iterchildren('menuitem'):
self._tag_menuitem(child, parent=menu.id)
xml_import._tag_menuitem = _tag_menuitem_extend
@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions
import json
from odoo.tools import ormcache
from odoo.tools.misc import file_path
from odoo.tools.config import config
class AnitaModeTemplate(models.Model):
'''
user theme style setting
'''
_name = 'anita_theme_setting.mode_template'
_description = 'anita Mode Template'
name = fields.Char(string="Name", required=True)
template = fields.Text(string="Template")
color_vars = fields.Text(
string="Color Vars", help="This is the default var values for this template")
template_path = fields.Char(
string="Template Path", help="This is the template path of the template")
_sql_constraints = [('name_unique', 'UNIQUE(name)', "Name Must Be Unique!")]
@ormcache()
def get_templates(self):
"""
get templates
:return:
"""
records = self.search([])
return {record.name: record for record in records}
@ormcache('self.id')
def get_template(self):
"""
get template
:return:
"""
return json.loads(self.template)
@ormcache('self.id')
def get_color_vars(self):
"""
get vars
:return:
"""
return json.loads(self.color_vars)
def refresh_templates(self):
"""
update template content
"""
records = self.search([])
for record in records:
if record.template_path:
tmp_path = file_path(record.template_path.lstrip('/'))
with open(tmp_path, 'r') as fd:
template = fd.read()
record.template = template
def _register_hook(self):
"""
stuff to do right after the registry is built
"""
if config.get('debug', False):
records = self.search([])
records.refresh_templates()
@api.model
def install_template(self, name, template_path, theme_mode_paths):
"""
install mode
:return:
"""
tmp_path = file_path(template_path.lstrip('/'))
if not tmp_path:
raise exceptions.ValidationError("Template Path is not valid!")
with open(tmp_path, 'r') as fd:
template = fd.read()
if not name:
raise exceptions.ValidationError(
'Template {template} must has a name!'.format(template=template_path))
template_record = self.search([('name', '=', name)])
if not template_record:
self.create([{
"name": name,
"template": template,
"template_path": template_path
}])
else:
template_record.write({
"name": name,
"template": template,
"template_path": template_path
})
for index, mode_path in enumerate(theme_mode_paths):
record = self.env['anita_theme_setting.default_mode_data'].install_mode(
mode_path, template_record)
if index == 0:
template_record.write({
"color_vars": record.template_var_vals
})
@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
import base64
from odoo.tools.mimetypes import guess_mimetype
from datetime import date, datetime
class AnitaUserSetting(models.TransientModel):
'''
anita user setting
'''
_inherit = 'res.config.settings'
theme_setting_mode = fields.Selection(
string="theme style mode",
config_parameter="anita_theme_setting.theme_setting_mode",
selection=[('system', 'system'),
('company', 'company'),
('user', 'user')],
default='system')
allow_debug = fields.Boolean(
string="allow debug",
config_parameter="anita_theme_setting.allow_debug",
default=False)
window_default_title = fields.Char(
string="login title",
config_parameter="anita_theme_setting.window_default_title",
default="Odoo")
powered_by = fields.Char(
string="powered by",
config_parameter="anita_theme_setting.powered_by",
default="Odoo")
@api.model
def get_theme_setting_mode(self):
'''
get theme setting mode
:return:
'''
config = self.env['ir.config_parameter'].sudo()
theme_setting_mode = config.get_param(
key='anita_theme_setting.theme_setting_mode', default='system')
return theme_setting_mode
def set_values_company_favicon(self):
'''
set the favicon of company
:return:
'''
company = self.sudo().env['res.company']
records = company.search([])
if len(records) > 0:
for record in records:
record.write({'favicon': self._set_web_favicon(original=True)})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
@api.model
def get_login_style(self):
'''
get login style
:return:
'''
ir_config = self.env['ir.config_parameter'].sudo()
login_style = ir_config.get_param(
key='anita_theme_setting.login_style', default='login_style1')
return login_style
def get_setting_record(self):
'''
get setting record
:return:
'''
record = self.env['anita_theme_setting.setting_base'].search([('owner', '=', False)], limit=1)
if not record:
record = self.env['anita_theme_setting.setting_base'].create({'owner': False})
return record
@api.model
def save_theme_setting(self, settings):
"""
save settings
"""
record = self.get_setting_record()
record.write(settings)
@api.model
def get_theme_setting(self):
"""
get theme settings
"""
record = self.get_setting_record()
result = record.read()[0]
result["current_theme_mode"] = record.current_theme_mode.id
result["current_theme_style"] = record.current_theme_style.id
for key, item in result.items():
if isinstance(item, datetime):
result[key] = fields.Datetime.to_string(item)
if isinstance(item, date):
result[key] = fields.Date.to_string(item)
return result
@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class AnitaThemeSettingBase(models.Model):
'''
anita Setting Base
'''
_name = 'anita_theme_setting.setting_base'
_description = 'Anita Setting Base'
owner = fields.Reference(
string="Owner",
selection=[('res.company', 'res.company'),
('res.users', 'res.users')],
default=False,
help="owner which this theme is create by, if it is false, it mean it is owned to the system!")
layout_mode = fields.Char(
string="Layout Mode",
default='Layout1')
control_panel_mode = fields.Char(
string="Control Panel Mode",
default='mode1')
login_style = fields.Char(
string="login style",
default='login_style1')
icon_policy = fields.Selection(
string="Menu Icon Policy",
selection=[
('web_icon', 'web_icon'),
('font_icon', 'font_icon'),
('svg_icon', 'svg_icon')],
default='svg_icon')
current_theme_mode = fields.Many2one(
comodel_name='anita_theme_setting.theme_mode',
string="Current Theme Mode")
current_theme_style = fields.Many2one(
string="Current Theme Style",
comodel_name="anita_theme_setting.theme_style",
domain="[('theme_mode', '=', current_theme_mode)]",
help="Just use when theme style mode is system")
dialog_animation_in_style = fields.Char(
string="Dialog Animation In Style",
default="animate__bounceIn")
dialog_animation_out_style = fields.Char(
string="Dialog Animation Out Style",
compute="_compute_dialog_animation_out_style")
button_style = fields.Char(
string="Button Style",
default="style1")
input_style = fields.Char(
string="Input Style",
default="style1")
tab_style = fields.Char(
string="Tab Style",
default="style1")
font_name = fields.Char(
string="Font Name",
default="Roboto", help="Roboto, Helvetica, Verdana, Tahoma, OpenSans, Poppins, NotoSansArabic")
loading_style = fields.Char(
string="Loading Style",
default="style1")
loading_svg = fields.Char(string="Loading SVG", compute="_compute_loading_svg")
checkbox_style = fields.Char(
string="Checkbox Style",
default="style1")
radio_style = fields.Char(
string="Radio Style",
default="style1")
separator_style = fields.Char(
string="Separator Style",
default="style1")
rtl_mode = fields.Boolean(
string="RTL MODE",
default=False)
allow_debug = fields.Boolean(
string="Allow Debug",
default=True)
@api.onchange('current_theme_mode')
def on_current_theme_mode_change(self):
"""
change the style
:return:
"""
if self.current_theme_mode.theme_styles:
self.current_theme_style = \
self.current_theme_mode.theme_styles[0].id
else:
self.current_theme_style = False
@api.depends('loading_style')
def _compute_loading_svg(self):
"""
compute loading svg
:return:
"""
for rec in self:
if rec.loading_style:
rec.loading_svg = "/anita_theme_setting/static/loadings/loading_%s.svg" % rec.loading_style
else:
rec.loading_svg = False
@api.depends('dialog_animation_in_style')
def _compute_dialog_animation_out_style(self):
'''
compute the dialog animation out style
:return:
'''
for record in self:
if record.dialog_animation_in_style:
record.dialog_animation_out_style = record.dialog_animation_in_style.replace('In', 'Out')
else:
record.dialog_animation_out_style = 'animate__bounceOut'
@@ -0,0 +1,314 @@
# -*- coding: utf-8 -*-
from inspect import trace
from math import fabs
from odoo import models, api, tools, fields, _
from odoo.tools.config import config
import traceback
import datetime
from datetime import date, datetime
import logging
_logger = logging.getLogger(__name__)
class AnitaThemeSettingManager(models.AbstractModel):
'''
user theme style setting
'''
_name = 'anita_theme_setting.setting_manager'
_description = 'theme setting manager'
def _register_hook(self):
"""
stuff to do right after the registry is built
"""
# check theme mode data
try:
# check mode data
owner = self.get_current_owner()
force = False
if config.get('debug', False):
self.env['anita_theme_setting.default_mode_data'].sudo().refresh_default_mode()
force = True
self.env["anita_theme_setting.theme_mode"].check_default_mode_data(owner, force)
except Exception as e:
traceback.print_stack(e)
_logger.error(e)
@api.model
def get_user_setting(self, get_style=True):
'''
get user setting
:return:
'''
rst = dict()
# just get the setting data
theme_setting_mode = \
self.env["res.config.settings"].sudo().get_theme_setting_mode()
# check mode data
owner = self.get_current_owner()
# get the setting data
if theme_setting_mode == 'system':
theme_settings = self.env["res.config.settings"].get_theme_setting()
elif theme_setting_mode == 'company':
theme_settings = self.env["res.company"].get_theme_setting()
else:
theme_settings = self.env["res.users"].get_theme_setting()
# deal the datetime data
for key, item in theme_settings.items():
if isinstance(item, datetime):
theme_settings[key] = fields.Datetime.to_string(item)
if isinstance(item, date):
theme_settings[key] = fields.Date.to_string(item)
all_modes = self.env["anita_theme_setting.theme_mode"].search(
[('owner', '=', owner)])
cur_style_id = theme_settings["current_theme_style"]
cur_mode_id = theme_settings["current_theme_mode"]
theme_mode = self.env["anita_theme_setting.theme_mode"].search(
[('id', '=', cur_mode_id)])
theme_style = self.env["anita_theme_setting.theme_style"].search(
[('id', '=', cur_style_id)])
cur_mode_name = theme_mode.name
if not cur_mode_id \
or cur_mode_id not in all_modes.ids:
if all_modes:
cur_mode_id = all_modes[0].id
cur_style_id = all_modes[0].theme_styles[0].id if all_modes[0].theme_styles else False
cur_mode_name = all_modes[0].name
theme_mode = all_modes[0]
theme_style = all_modes[0].theme_styles[0] if all_modes[0].theme_styles else False
else:
cur_mode_id = False
cur_style_id = False
cur_mode_name = False
theme_mode = self.env["anita_theme_setting.theme_mode"]
if not cur_style_id:
theme_styles = theme_mode.theme_styles
theme_style = theme_styles[0] if theme_styles else self.env["anita_theme_setting.theme_style"]
cur_style_id = theme_style.id if theme_style else False
rst['settings'] = theme_settings
rst['cur_mode_id'] = cur_mode_id
rst['cur_style_id'] = cur_style_id
# used to set the body class
rst['cur_mode_name'] = cur_mode_name
rst['theme_setting_mode'] = theme_setting_mode
rst["window_default_title"] = self.env['ir.config_parameter'].sudo().get_param(
"anita_theme_setting.window_default_title", "Odoo")
rst["powered_by"] = self.env['ir.config_parameter'].sudo().get_param(
"anita_theme_setting.powered_by", "Odoo")
# check the user is anita
rst['is_anita'] = self.env.user._is_admin()
# add font info
font_name = theme_settings.get("font_name", False)
if font_name:
font_type_css = "*:not(.fa):not(.oi) {font-family: " + font_name + ", sans-serif !important;}"
else:
font_type_css = ""
rst['theme_modes'] = self.get_all_mode_data(owner)
# get the style
if get_style:
if theme_style:
style_datas = [font_type_css]
if theme_style:
style_datas += theme_style.get_styles_txt()
style_txt = '\n'.join(style_datas)
rst['style_txt'] = style_txt
if not theme_style.background_image:
# get from theme mode
rst['mode_style_css'] = theme_mode.get_mode_style_css()
else:
# get from style
rst['mode_style_css'] = theme_style.get_mode_style_css()
else:
rst['style_txt'] = ""
rst['mode_style_css'] = ""
return rst
@api.model
def get_font_link(self):
"""
get font link
:return:
"""
theme_settings = self.get_theme_setting()
result = ""
if theme_settings.get('font_name', False):
result = '/anita_theme_setting/static/fonts/{font_name}/fonts.css'.format(font_name=theme_settings.get('font_name'))
return result
@api.model
def get_theme_setting(self):
"""
get theme setting
:return:
"""
# just get the setting data
theme_setting_mode = \
self.env["res.config.settings"].sudo().get_theme_setting_mode()
if theme_setting_mode == 'system':
theme_settings = self.env["res.config.settings"].get_theme_setting()
elif theme_setting_mode == 'company':
theme_settings = self.env["res.company"].get_theme_setting()
else:
theme_settings = self.env["res.users"].get_theme_setting()
return theme_settings
@api.model
def get_font_name(self):
"""
get font name
"""
theme_settings = self.get_theme_setting()
return theme_settings.get('font_name')
@api.model
def update_cur_style(self, mode_id, style_id):
'''
update user cur mode
:return:
'''
ir_config = self.env['ir.config_parameter'].sudo()
setting_mode = self.env["res.config.settings"].get_theme_setting_mode()
if setting_mode == "system":
ir_config.set_param("anita_theme_setting.current_theme_mode", mode_id)
ir_config.set_param("anita_theme_setting.current_theme_style", style_id)
elif setting_mode == "company":
company = self.env.user.company_id
company.setting_id.current_theme_mode = mode_id
company.setting_id.current_theme_style = style_id
elif setting_mode == "user":
user_id = self.env.user.id
user = self.env["res.users"].browse(user_id)
user.setting_id.current_theme_mode = mode_id
user.setting_id.current_theme_style = style_id
else:
assert False
@api.model
def save_style_data(self, style_id, style_data, owner):
'''
save style datas
:param style_id:
:param style_data:
:param owner:
:return:
'''
theme_style = self.env["anita_theme_setting.theme_style"].browse(style_id)
theme_style.ensure_one()
theme_mode = theme_style.theme_mode
# save current style just when type is same
mode_id = theme_mode.id
if owner:
if owner.startswith('res.users'):
parts = owner.split(',')
user = self.env["res.users"].browse(int(parts[1].strip()))
if user:
user.ensure_setting_id()
user.setting_id.current_theme_mode = mode_id
user.setting_id.current_theme_style = style_id
elif owner.startswith('res.company'):
parts = owner.split(',')
company = self.env["res.company"].browse(int(parts[1].strip()))
if company:
company.ensure_setting_id()
company.setting_id.current_theme_mode = mode_id
company.setting_id.current_theme_style = style_id
else:
system_setting = self.env["res.config.settings"].get_setting_record()
system_setting.current_theme_mode = mode_id
system_setting.current_theme_style = style_id
# as the image is direct change from database so not need to update it
var_infos = theme_style.mapped('groups.sub_groups.style_items.vars')
var_cache = {var_info.id: var_info for var_info in var_infos}
for data in style_data:
var_id = data["id"]
tmp_var = var_cache[var_id]
tmp_var.write({
"color": data.get('color', False)
})
if theme_style.background_image:
mode_style_cs = theme_style.get_mode_style_css()
else:
mode_style_cs = theme_mode.get_mode_style_css()
# return the style data
return {
'theme_style': theme_style.get_style(),
'mode_style_css': mode_style_cs
}
def get_current_owner(self):
"""
get current owner
:return:
"""
theme_setting_mode = \
self.env["res.config.settings"].sudo().get_theme_setting_mode()
# check mode data
owner = False
if theme_setting_mode == 'system':
owner = False
elif theme_setting_mode == 'company':
owner = 'res.company, {company_id}'.format(
company_id=self.env.user.company_id.id)
elif theme_setting_mode == 'user':
owner = 'res.users, {user_id}'.format(user_id=self.env.user.id)
return owner
@tools.ormcache('owner')
@api.model
def get_all_mode_data(self, owner):
"""
get all mode data
"""
all_modes = self.env["anita_theme_setting.theme_mode"].search([('owner', '=', owner)])
result = []
for mode in all_modes:
result.append(mode.get_mode_data()[0])
return result
@api.model
def save_settings(self, settings):
"""
save the settings
"""
# get the setting mode
setting_mode = self.env["res.config.settings"].get_theme_setting_mode()
if setting_mode == "system":
self.env["res.config.settings"].save_theme_setting(settings)
elif setting_mode == "company":
self.env["res.company"].save_theme_setting(settings)
elif setting_mode == "user":
self.env["res.users"].save_theme_setting(settings)
else:
assert False
return self.get_theme_setting()
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions
import json
from odoo.tools import ormcache
class AnitaTemplateVarVal(models.Model):
'''
user theme style setting
'''
_name = 'anita_theme_setting.template_var_val'
_description = 'anita Mode Template'
theme_mode = fields.Many2one(string="Theme Mode", comodel_name="anita_theme_setting.theme_mode")
name = fields.Char(string="Name", required=True)
group = fields.Char(string="Group", required=True)
value = fields.Char(string="Value", required=True)
disable_opacity = fields.Boolean(string="Disable Opacity", default=False)
description = fields.Char(string="Description")
@@ -0,0 +1,588 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions, tools
import json
import re
from odoo.tools.misc import file_path
from odoo.tools.config import config
from .anita_utility import apply_alpha
try:
import sass as libsass
except ImportError:
libsass = None
import logging
_logger = logging.getLogger(__name__)
class AnitaThemeMode(models.Model):
'''
user theme style setting
'''
_name = 'anita_theme_setting.theme_mode'
_description = 'anita theme mode'
name = fields.Char(string="Name", required=True)
sequence = fields.Integer(string="Mode Sequence", default=0)
theme_styles = fields.One2many(
string="Theme Styles",
comodel_name="anita_theme_setting.theme_style",
inverse_name="theme_mode")
mode_template = fields.Many2one(
comodel_name="anita_theme_setting.mode_template",
string="Mode Template")
template_var_vals = fields.One2many(
comodel_name="anita_theme_setting.template_var_val",
inverse_name="theme_mode",
string="Template Vars")
# if owner is False, so the style own to global
owner = fields.Reference(
string="Owner",
selection=[('res.company', 'res.company'),
('res.users', 'res.users')],
default=False,
help="owner which this theme is create by, if it is false, it mean it is owned to the system!")
mode_style_css = fields.Text(string="Mode Style Css")
compiled_mode_style_css = fields.Text(string="Compiled mode style css")
is_default = fields.Boolean(string="Is Default", default=False)
version = fields.Char(
string="Version", help="use version to check update or not")
_sql_constraints = [('theme_mode_name_unique', 'UNIQUE(name, owner)',
"Theme mode name and owner must unique")]
def get_mode_preview_data(self, style_id=False):
"""
get mode preview data
:return:
"""
if self.mode_template:
if style_id:
theme_style = self.env['anita_theme_setting.theme_style'].browse(
style_id)
if theme_style.is_semi_transparent():
result = theme_style.get_mode_style_css()
else:
result = self.get_mode_style_css()
else:
result = self.get_mode_style_css()
else:
result = ""
mode_name = 'body.{name}'.format(name=self.name)
# replace mode_name with body.preview
result = result.replace(mode_name, 'body.preview')
return result
@api.onchange('mode_template')
def on_mode_template_changed(self):
"""
compute mode style
:return:
"""
# check id is instance of NewId
if self.mode_template:
scss_vars = self.mode_template.get_color_vars()
template_var_vals = [(5, 0, 0)]
for group_name in scss_vars:
var_items = scss_vars[group_name]
for var_item in var_items:
template_var_vals.append((0, 0, {
"name": var_item["name"],
"group": group_name,
"value": var_item["value"],
"disable_opacity": var_item.get("disable_opacity", False),
"description": var_item["description"]
}))
self.template_var_vals = template_var_vals
else:
self.template_var_vals = [(5, 0, 0)]
def check_template_vars(self):
"""
check style css
:return:
"""
return
@tools.ormcache('self.id')
def get_scss_vars_cache(self):
"""
get scss vars
:return:
"""
records = self.template_var_vals.read(['name', 'value'])
return {record['name']: record['value'] for record in records}
@tools.ormcache('self.id')
def get_scss_var_record_cache(self):
"""
get scss vars
:return:
"""
records = self.template_var_vals
return {record.name: {
"name": record.name,
"value": record.value,
"disable_opacity": record.disable_opacity,
} for record in records}
def _compile_scss(self, alpha=False):
"""
This code will compile valid scss into css.
Simply copied and adapted slightly
"""
self.ensure_one()
scss_source = self.mode_template.template
scss_vars = []
var_val_cache = self.get_scss_var_record_cache()
for var_name in var_val_cache:
var_record = var_val_cache[var_name]
name = var_record.get("name")
if alpha and alpha < 1:
color = apply_alpha(var_record.get('name'), var_record.get('value'), alpha)
else:
color = var_record.get('value')
# new color
scss_vars.append('{name}: {value};'.format(
name=name, value=color))
# origin color, some element disable alpha
scss_vars.append('{name}: {value};'.format(
name=name.replace('$', '$_'), value=var_record.get('value')))
precision = 8
output_style = 'expanded'
bootstrap_scss_path = file_path('web/static/lib/bootstrap/scss')
mode_selectors = "$mode_selectors: 'body.{name}';".format(name=self.name)
extra_vars = [
mode_selectors,
"$compile_normal: 1;",
"$semi_transparent: {semi_trans};".format(semi_trans=True if alpha < 1.0 else False),
]
total_vars = extra_vars + scss_vars
normal_source = '\n'.join(total_vars) + '\n' + scss_source if scss_source else ''
# compile normal
results = []
try:
result = libsass.compile(
string=normal_source,
include_paths=[bootstrap_scss_path],
output_style=output_style,
precision=precision)
# remove the import bootstrap
index = result.find("body.{name}".format(name=self.name))
result = result[index:]
results.append(result)
except libsass.CompileError as e:
_logger.error(e.args[0])
# compile for background
if alpha < 1.0:
scss_vars = []
for var_name in var_val_cache:
var_record = var_val_cache[var_name]
name = var_record.get("name")
# new color
scss_vars.append('{name}: {value};'.format(name=name, value=var_record.get('value')))
# origin color
scss_vars.append('{name}: {value};'.format(name=name.replace('$', '$_'), value=var_record.get('value')))
mode_selectors = "$mode_selectors: 'body.{name} .o_dialog_container';".format(name=self.name)
extra_vars = [
mode_selectors,
"$compile_normal: 0;",
"$semi_transparent: {semi_trans};".format(semi_trans=1 if alpha < 1.0 else 0),
]
total_vars = extra_vars + scss_vars
normal_source = '\n'.join(total_vars) + '\n' + str(scss_source)
try:
result = libsass.compile(
string=normal_source,
include_paths=[bootstrap_scss_path],
output_style=output_style,
precision=precision)
# remove the import bottstrap
index = result.find('body.{name} .o_dialog_container'.format(name=self.name))
result = result[index:]
results.append(result)
except libsass.CompileError as e:
_logger.error(e.args[0])
return '\n'.join(results)
@api.model
def compute_mode_style_url(self):
"""
compute mode style url
:return:
"""
for record in self:
record.mode_style_url = \
'/web/image/anita_theme_setting.anita_theme_setting/{var_id}/icon128x128'.format(
var_id=record.id)
def get_default_mode_style_text(self, theme_mode_info):
"""
get default mode data
:return:
"""
if self.name == 'normal':
return
color_vars = theme_mode_info["template_vars"]
mode_template = theme_mode_info["mode_template"]
if not mode_template:
raise exceptions.ValidationError('Can not find the theme template!')
template = mode_template.template
# css template data
template = template.replace('$mode_name', self.name)
# replace the var
for var_nam in color_vars:
template = str(template).replace(var_nam, color_vars[var_nam])
return template
@api.model
def check_default_mode_data(self, owner=False, force = False):
'''
check default mode data
:return:
'''
modes = self.search([('owner', '=', owner)])
olde_mode_cache = {mode.name: mode for mode in modes}
default_modes = \
self.env["anita_theme_setting.default_mode_data"].search([])
# create new mode if the mode do not exits
for default_mode in default_modes:
if not default_mode.data:
continue
# need to reload the mode data
mode_data = default_mode.get_mode_data()
if default_mode.name not in olde_mode_cache:
mode_data["name"] = default_mode["name"]
# create new mode use the mode data
record = self.create_mode(mode_data, owner)
if not record.mode_template:
continue
# check the templates
templates = self.env["anita_theme_setting.mode_template"].get_templates(
)
template = mode_data.get('mode_template', False)
if template and template not in templates:
raise exceptions.ValidationError(
'{template} do not exits!'.format(template=mode_data["template"]))
template = templates[mode_data["mode_template"]]
record.mode_template = template.id
# check vars are defined in the template vars
record.check_template_vars()
else:
# check version, if the version is different
mode = olde_mode_cache[default_mode.name]
if force or mode.version != mode_data["version"]:
# update template vars
temlate_var_vals = mode_data["template_var_vals"]
mode.update_template_var_values(temlate_var_vals)
# update template
mode_templates = self.env["anita_theme_setting.mode_template"].get_templates(
)
if mode_data.get("mode_template", False):
template_name = mode_data["mode_template"]
mode.mode_template = \
mode_templates.get(template_name, False)
mode.version = mode_data["version"]
mode.check_mode_data()
# recompile the template
mode.compiled_mode_style_css = False
# unlink the invalid mode
for default_mode in default_modes:
if not default_mode.data:
default_mode.unlink()
def create_mode(self, mode_config, owner=False, is_default=True):
'''
create mode
:param mode style txt:
:param mode_data:
:param owner:
:param is_default:
:return:
'''
mode_templates = \
self.env["anita_theme_setting.mode_template"].get_templates()
mode_template = mode_config.get('mode_template', False)
if mode_template and mode_template not in mode_templates:
raise exceptions.ValidationError('Can not find mode template {mode_template}'.format(
mode_template=mode_template))
# convert to cache data
template_var_vals = mode_config.get('template_var_vals', {})
var_val_cache = {}
for group in template_var_vals:
var_items = template_var_vals[group]
for var_item in var_items:
var_val_cache[var_item["name"]] = var_item["value"]
theme_styles = mode_config['theme_styles']
theme_style_array = []
for theme_style in theme_styles:
groups = theme_style["groups"]
group_datas = []
for group_index, var_items in enumerate(groups):
sub_groups = var_items["sub_groups"]
sub_group_array = []
for sub_group_index, sub_group in enumerate(sub_groups):
# create sub group
item_array = []
style_items = sub_group["style_items"]
for item_index, style_item in enumerate(style_items):
var_val_array = []
var_infos = style_item["vars"]
for var_index, var_info in enumerate(var_infos):
color = var_info.get("color", False)
ref_var = var_info.get("ref_var", False)
if ref_var:
if ref_var not in var_val_cache:
color = False
_logger.info(
"Can not find ref var {ref_var}".format(ref_var=ref_var))
else:
color = var_val_cache[ref_var]
var_val_array.append((0, 0, {
"name": var_info["name"],
"type": var_info["type"],
"sequence": var_index,
"is_default": is_default,
"color": color,
"image": var_info.get('image', False),
"image_url": var_info.get('image_url', False),
"svg": var_info.get('svg', False),
"identity": var_info.get('identity', False)
}))
item_array.append((0, 0, {
"name": style_item["name"],
"is_default": is_default,
"sequence": item_index,
"val_template": style_item["val_template"],
"sub_group": sub_group["name"],
"vars": var_val_array,
"selectors": json.dumps(style_item["selectors"])
}))
sub_group_array.append((0, 0, {
"name": sub_group["name"],
"sequence": sub_group_index,
"style_items": item_array,
"is_default": is_default
}))
group_datas.append((0, 0, {
"name": var_items["name"],
"sequence": group_index,
"is_default": is_default,
"sub_groups": sub_group_array
}))
theme_style_array.append((0, 0, {
"name": theme_style["name"],
"is_default": is_default,
"groups": group_datas
}))
vals = []
template_var_vals = mode_config.get('template_var_vals', {})
for group_name in template_var_vals:
var_items = template_var_vals[group_name]
for var_item in var_items:
vals.append((0, 0, {
"name": var_item["name"],
"group": group_name,
"value": var_item["value"],
"disable_opacity": var_item.get("disable_opacity", False),
"description": var_item["description"]
}))
mode_template = mode_config.get("mode_template", False)
return self.env["anita_theme_setting.theme_mode"].create([{
"name": mode_config["name"],
"theme_styles": theme_style_array,
"is_default": is_default,
"version": mode_config["version"],
"mode_template": mode_templates[mode_template].id if mode_template else False,
"owner": owner,
"template_var_vals": vals
}])
def get_mode_data(self):
'''
get the mode data
:return:
'''
self.ensure_theme_style()
rst = []
for record in self:
tmp_data = record.read(
['name', 'is_default', 'sequence', 'version'])[0]
tmp_data['theme_styles'] = record.theme_styles.get_styles()
rst.append(tmp_data)
return rst
def delete_mode(self):
"""
delete the mode
:return:
"""
self.unlink()
def check_style_css(self):
"""
check style css
:return:
"""
if self.mode_template and not self.compiled_mode_style_css:
self.compiled_mode_style_css = self._compile_scss()
def update_template_var_values(self, template_var_vals):
"""
update template var values
:param temlate_var_vals:
:return:
"""
self.ensure_one()
vals = [(5, 0, 0)]
for group_name in template_var_vals:
var_items = template_var_vals[group_name]
for var_item in var_items:
vals.append((0, 0, {
"name": var_item["name"],
"group": group_name,
"value": var_item["value"],
"description": var_item.get("description")
}))
self.template_var_vals = vals
def get_mode_style_css(self):
"""
get mode style css
"""
if config.get('debug', False) or not self.compiled_mode_style_css:
self.compiled_mode_style_css = self._compile_scss()
return self.compiled_mode_style_css
def check_mode_data(self):
'''
check the mode data
:return:
'''
self.ensure_one()
default_modes = self.env[
"anita_theme_setting.default_mode_data"].search([])
default_mode_cache = {
default_mode.name: default_mode for default_mode in default_modes}
mode_name = self.name
if mode_name not in default_mode_cache:
return
theme_styles = self.theme_styles
theme_style_cache = {
theme_style.name: theme_style for theme_style in theme_styles if theme_style.is_default}
default_mode = default_mode_cache[mode_name]
default_mode_data = default_mode.get_mode_data()
theme_styles = default_mode_data['theme_styles']
for theme_style in theme_styles:
# create new style
style_name = theme_style['name']
if style_name not in theme_style_cache:
self.env["anita_theme_setting.theme_style"].create_style(
self.id, theme_style)
else:
# check the style data, may be one more style with the same name
tmp_style = theme_style_cache[style_name]
tmp_style.check_groups(default_mode, theme_style["groups"])
def get_mode_edit_action(self, style_id):
"""
get theme mode edit action
"""
self.ensure_one()
form_id = self.env.ref("anita_theme_setting.edit_theme_mode_form").id
var_vals = []
for var_val in self.template_var_vals:
var_vals.append((0, 0, {
"name": var_val.name,
"group": var_val.group,
"value": var_val.value,
"disable_opacity": var_val.disable_opacity,
"description": var_val.description
}))
return {
"type": "ir.actions.act_window",
"res_model": "anita_theme_setting.theme_mode",
"views": [[form_id, "form"]],
"res_id": self.id,
"view_mode": "form",
"target": "new",
"context": {
"default_name": self.name,
"default_mode_template": self.mode_template.id,
"sequnce": self.sequence,
"template_var_vals": var_vals,
"default_style_id": style_id
}
}
@api.model
def get_new_mode_action(self):
"""
get create new mode action
"""
form_id = self.env.ref("anita_theme_setting.new_theme_mode_form").id
return {
"type": "ir.actions.act_window",
"res_model": "anita_theme_setting.theme_mode",
"views": [[form_id, "form"]],
"view_mode": "form",
"target": "new"
}
def ensure_theme_style(self):
"""
create a empty theme style
"""
if not self.theme_styles:
self.env["anita_theme_setting.theme_style"].create_style(
self.id, {
"name": "style1",
"is_default": False,
"groups": []
})
@@ -0,0 +1,506 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _, exceptions, tools
import json
import base64
from odoo.tools.config import config
from .anita_utility import apply_alpha
import traceback
import logging
_logger = logging.getLogger(__name__)
class AnitaThemeStyle(models.Model):
'''
Theme style
'''
_name = 'anita_theme_setting.theme_style'
_description = 'awesome theme style'
_order = 'sequence asc, id asc'
name = fields.Char(string="style name", required=True)
theme_mode = fields.Many2one(
comodel_name="anita_theme_setting.theme_mode",
string="theme mode",
ondelete="cascade")
mode_css = fields.Text(string="mode style scss")
style_css = fields.Text(string="style css")
is_default = fields.Boolean(string="is default", default=False)
background_image = fields.Binary(string="background image")
opacity = fields.Float(string="opacity", default=1.0)
owner = fields.Reference(
string="owner",
selection=[
('res.company', 'res.company'),
('res.users', 'res.users')],
default=False,
help="Owner which this theme is create by")
sequence = fields.Integer(string="style sequence", default=0)
groups = fields.One2many(
string="Groups",
comodel_name="anita_theme_setting.style_item_group",
inverse_name="theme_style")
side_bar_style = fields.Selection(
string="side bar style",
selection=[
('style1', 'style1'),
('style2', 'style2'),
('style3', 'style3')])
sidebar_width = fields.Integer(string="sidebar width", default=80)
sidebar_menu_width = fields.Integer(
string="sidebar menu width", default=220)
@tools.ormcache('self.id')
def get_style(self):
'''
get simple style data
:return:
'''
self.ensure_one()
result = self.read(['id', 'name', 'is_default'])[0]
result["groups"] = self.groups.get_group_data()
result["theme_mode"] = self.theme_mode.name
# get background image ulr
if self.background_image:
result["background_image"] = self.get_background_image_url()
else:
result["background_image"] = False
result["opacity"] = self.opacity
identities = dict()
groups = result["groups"]
# collect identity
for group in groups:
sub_groups = group["sub_groups"]
for sub_group in sub_groups:
style_items = sub_group["style_items"]
for item in style_items:
if not item["vars"]:
continue
# collect the identity
var_infos = item["vars"]
for var in var_infos:
if var.get('identity', False):
if self.is_semi_transparent():
identities[var['identity']] = apply_alpha(var['name'], var['color'], self.opacity, var.get('color_type'))
else:
identities[var['identity']] = var['color']
result["identities"] = identities
return result
def get_styles(self):
'''
get mode datas
:return:
'''
results = []
for record in self:
results.append(record.get_style())
return results
def _get_style_data(self):
'''
get mode datas
:return:
'''
self.ensure_one()
results = []
result = self.read(['name', 'is_default', 'sequence'])[0]
if self.background_image:
result["background_image"] = self.get_background_image_url()
result["groups"] = self.groups.get_group_data()
results.append(result)
return results
def get_styles_txt(self):
'''
get style txt
:return:
'''
self.ensure_one()
mode_prefix = 'body.' + self.theme_mode.name + ' '
result = []
groups = self.groups
for group in groups:
for sub_group in group.sub_groups:
style_items = sub_group.style_items
for style_item in style_items:
val = style_item.val
# if the val is same as the ref var, then ignore it
if not val or val == "" or not style_item.selectors:
continue
try:
selector = mode_prefix + \
','.join(json.loads(style_item["selectors"]))
tmp_txt = selector + " {" + val + "}"
result.append(tmp_txt)
except Exception as error:
traceback.print_stack(e)
_logger.info(error)
# append the background image
if self.background_image:
background_image_url = self.get_background_image_url()
if background_image_url:
result.append(
mode_prefix + " {background-image: url('" +
background_image_url + "') !important;")
return result
def is_semi_transparent(self):
'''
is semi transparent
:return:
'''
self.ensure_one()
return self.opacity < 1.0
def get_mode_style_css(self):
'''
get mode styles
:return:
'''
self.ensure_one()
if not self.is_semi_transparent():
return self.theme_mode.get_mode_style_css()
else:
if config.get('debug', False) or not self.mode_css:
self.mode_css = self.theme_mode._compile_scss(self.opacity or 1)
return self.mode_css
def _compute_var_val(self, style_item_info):
'''
compute the val, use by the
:return:
'''
self.ensure_one()
background_image = self.background_image
scss_var_cache = self.theme_mode.get_scss_vars_cache()
opacity = self.opacity
vars_cache = dict()
all_color = True
all_ref = True
all_same = True
for var in style_item_info.get('vars', []):
name = var.get('name', False)
type = var.get('type', False)
if type == 'color':
# check if has opacity
color = var.get('color')
ref_var = var.get('ref_var', False)
if ref_var:
ref_color = scss_var_cache[ref_var]
if ref_color != color:
all_same = False
else:
all_ref = False
try:
if background_image:
if not var.get('disable_opacity', False):
color = apply_alpha(name, color, opacity, var.get('color_type'))
except Exception as e:
traceback.print_stack(e)
_logger.error("apply alpha error {e}".format(e=e))
vars_cache[name] = color
elif var.type == 'image':
vars_cache[name] = var.get('image_file_url')
all_color = False
elif var.type == 'image_url':
vars_cache[name] = var.get('image_url')
all_color = False
else:
vars_cache[name] = var.get('svg')
all_color = False
# if all the color are the original color, them ignore it
if all_color and all_ref and all_same:
style_item_info['val'] = False
else:
try:
val_template = style_item_info.get('val_template', False)
style_item_info['val'] = val_template.format(**vars_cache)
except Exception as error:
style_item_info['val'] = False
traceback.print_stack(e)
_logger.info('parse var value error, item name {item_name}->{error}'.format(
item_name=style_item_info['name'], error=error))
def post_deal_preview_data(self, style_item_infos, font_info, preview = False):
'''
get mode datas
:return:
'''
self.ensure_one()
if preview:
mode_prefix = 'body.preview '
else:
mode_prefix = 'body.' + self.theme_mode.name + ' '
result = [font_info]
groups = self.groups
for group in groups:
for sub_group in group.sub_groups:
style_items = sub_group.style_items
for style_item in style_items:
if str(style_item.id) not in style_item_infos:
val = style_item.val
if val == "":
continue
if not style_item.selectors:
continue
try:
selector = mode_prefix + \
','.join(json.loads(style_item["selectors"]))
tmp_txt = selector + " {" + val + "}"
result.append(tmp_txt)
except Exception as error:
traceback.print_stack(e)
_logger.info(error)
else:
style_item = style_item_infos[str(style_item.id)]
# compute the val
self._compute_var_val(style_item)
val = style_item.get('val', False)
# if the val is same as the ref var, then ignore it
selectors = style_item.get('selectors', False)
if not val or val == "" or not selectors:
continue
try:
# check selector is a array
if not isinstance(selectors, list):
selectors = [selectors]
selector = mode_prefix + ','.join(selectors)
tmp_txt = selector + " {" + val + "}"
result.append(tmp_txt)
except Exception as error:
traceback.print_stack(error)
_logger.info(error)
# append the background image
if self.background_image:
background_image_url = self.get_background_image_url()
if background_image_url:
result.append(
mode_prefix + " {background-image: url('" +
background_image_url + "') !important;")
return "\n".join(result)
@api.model
def delete_style(self, style_id):
'''
delete styel of user
:return:
'''
record = self.search([('id', '=', style_id)])
record.unlink()
def check_groups(self, default_mode, groups):
'''
check the style group
:param groups:
:return:
'''
self.ensure_one()
new_group_cache = {group["name"]: group for group in groups}
old_group_cache = {group.name: group for group in self.groups}
for group_index, name in enumerate(new_group_cache):
default_group = new_group_cache[name]
if name not in old_group_cache:
self.env['anita_theme_setting.style_item_group']\
.create_group_from_default_data(self, default_group, group_index)
else:
# update the style item
tmp_group = old_group_cache[name]
tmp_group.write({
"name": name,
"is_default": True,
"sequence": group_index,
})
# check group item data
sub_group_items = default_group["sub_groups"]
tmp_group.check_sub_group(default_mode, sub_group_items)
# delete the style item
for tmp_group_name in old_group_cache:
if tmp_group_name not in new_group_cache \
and old_group_cache[tmp_group_name].is_default:
old_group_cache[tmp_group_name].unlink()
def create_style(
self, model_id, style_data, owner=False, is_default=True):
'''
create theme style from default info
:return:
'''
default_groups = style_data['groups']
mode = self.env['anita_theme_setting.theme_mode'].browse(model_id)
scss_vars_cache = mode.get_scss_vars_cache()
group_datas = []
for group_index, group in enumerate(default_groups):
sub_groups = group["sub_groups"]
sub_group_array = []
for sub_group_index, sub_group in enumerate(sub_groups):
style_items = sub_group["style_items"]
style_item_datas = []
for style_item_index, style_item in enumerate(style_items):
selectors = json.dumps(style_item['selectors'])
var_vals = []
var_infos = style_item['vars']
for var_info in var_infos:
ref_var = var_info.get('ref', False)
if ref_var:
if ref_var in scss_vars_cache:
raise exceptions.ValidationError(
_("The variable %s has been used") % ref_var)
else:
color = scss_vars_cache[ref_var]
else:
color = var_info.get('color', False)
var_vals.append((0, 0, {
"name": var_info["name"],
"is_default": is_default,
"type": var_info["type"],
"color": color,
"image": var_info.get('image', False),
"image_url": var_info.get('image_url', False),
"svg": var_info.get('svg', False),
"identity": var_info.get('identity', False)
}))
tmp_style_item = dict()
tmp_style_item['selectors'] = selectors
tmp_style_item['sequence'] = style_item_index
tmp_style_item['name'] = style_item["name"]
tmp_style_item["is_default"] = is_default
tmp_style_item["val_template"] = style_item["val_template"]
tmp_style_item["vars"] = var_vals
style_item_datas.append((0, 0, tmp_style_item))
sub_group_array.append((0, 0, {
"name": sub_group["name"],
"sequence": sub_group_index,
"style_items": style_item_datas,
"is_default": True
}))
group_datas.append((0, 0, {
"name": group["name"],
"sequence": group_index,
"sub_groups": sub_group_array,
"is_default": True
}))
return self.env['anita_theme_setting.theme_style'].create({
"name": style_data["name"],
"theme_mode": model_id,
"is_default": is_default,
"groups": group_datas,
"owner": owner
})
@api.model
def add_new_style(
self, mode_id, style_data, owner=False, is_default=False):
'''
add new style, get the first style as the default, call from the front web
:return:
'''
# if not owner, use the current owner
if not owner:
owner = self.env["anita_theme_setting.setting_manager"].get_current_owner()
theme_style = self.create_style(
mode_id, style_data, owner, is_default)
# return the new style data
style_data = theme_style.get_styles()[0]
return style_data
def clone_style(self, owner=False):
'''
clone style
:return:
'''
self.ensure_one()
if not owner:
owner = self.env["anita_theme_setting.setting_manager"].get_current_owner()
style_data = self.get_styles()[0]
return self.add_new_style(self.theme_mode.id, style_data, owner)
@api.model
def import_new_style(self, mode_id, wizard_id):
"""
import new style
:param mode_id:
:param wizard_id:
:return:
"""
wizard = self.env["anita_theme_setting.import_theme_style"].browse(
wizard_id)
style_data = json.loads(
base64.decodebytes(wizard.file).decode('utf-8'))
return self.add_new_style(mode_id, style_data)
def set_bk_image(self, wizard_id):
"""
set background image
"""
self.ensure_one()
wizard = self.env["anita_theme_setting.background_image_wizard"].browse(
wizard_id)
self.background_image = wizard.data
self.opacity = wizard.opacity
# reutrn the image url
url = self.get_background_image_url()
# clear cache
self.clear_caches()
return url
def get_background_image_url(self):
"""
get image url
"""
self.ensure_one()
if self.background_image:
return 'web/image/anita_theme_setting.theme_style/' + str(self.id) + '/background_image'
else:
return False
def clear_style_css(self):
"""
clear style css when style change
"""
self.ensure_one()
self.style_css = False
@@ -0,0 +1,332 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions
import logging
import json
from .anita_utility import apply_alpha
_logger = logging.getLogger(__name__)
class AnitaThemeStyleItem(models.Model):
'''
user theme style setting
'''
_name = 'anita_theme_setting.style_item'
_order = 'sequence asc'
_description = "style item"
sub_group = fields.Many2one(
comodel_name="anita_theme_setting.style_item_sub_group", string="sub group")
theme_mode = fields.Many2one(
comodel_name="anita_theme_setting.theme_mode",
related="sub_group.group.theme_style.theme_mode",
store=True,
string="theme mode")
theme_style = fields.Many2one(
comodel_name="anita_theme_setting.theme_style",
related = "sub_group.group.theme_style",
string="theme style")
name = fields.Char(string="name", required=True, default="var1")
val = fields.Char(string="val", compute="_compute_val")
sequence = fields.Integer(string="sequence", default=0)
mode = fields.Selection(
selection=[('override', 'override'), ('primary', 'primary')],
string="mode",
default="override",
help="override or primary, override is override the default style and primary is create a new style")
val_template = fields.Char(
string="val template",
required=True,
default="background: {var1}", help="Save the last val")
vars = fields.One2many(
string="vars",
comodel_name="anita_theme_setting.theme_var",
inverse_name="style_item")
selectors = fields.Char(string="selectors", required=True)
is_default = fields.Boolean(string="Is Default", default=True)
type = fields.Selection(
string="type",
selection=[('normal', 'normal'),
('login', 'login')],
default='normal')
login_style = fields.Char(string="login style", default="login_style1")
def check_vars(self, default_mode, sub_group_id, default_vars):
'''
check vars
:return:
'''
# check vars
self.ensure_one()
var_val_cache = default_mode.get_var_val_cache()
# get current var cache
old_var_cache = {var.name: var for var in self.vars}
default_var_cache = \
{default_var["name"]: default_var for default_var in default_vars}
tmp_vals = []
for default_var in default_vars:
color = default_var.get("color", False)
ref_var = default_var.get("ref_var",False)
if ref_var:
if ref_var not in var_val_cache:
color = False
_logger.info("ref var {ref_var} not in var cache".format(ref_var=ref_var))
else:
color = var_val_cache[ref_var]
# item exits must keep
if default_var["name"] not in old_var_cache:
tmp_vals.append({
"sub_group": sub_group_id,
"name": default_var["name"],
"style_item": self.id,
"type": default_var["type"],
"color": color,
"image": default_var.get('image', False),
"image_url": default_var.get('image_url', False),
"svg": default_var.get('svg', False),
"is_default": default_var.get('is_default', True),
"identity": default_var.get('identity', False),
"ref_var": ref_var,
"disable_opacity": default_var.get('disable_opacity', False),
})
else:
# keep the val, maybe user will
tmp_var = old_var_cache[default_var["name"]]
tmp_var.write({
"type": default_var["type"],
"is_default": default_var.get('is_default', True),
"identity": default_var.get('identity', False),
"color": color,
"image": default_var.get('image', False),
"image_url": default_var.get('image_url', False),
"svg": default_var.get('svg', False),
"ref_var": ref_var,
"disable_opacity": default_var.get('disable_opacity', False),
})
# create the missing vars
self.env["anita_theme_setting.theme_var"].create(tmp_vals)
# unlink the old var
for name in old_var_cache:
if name not in default_var_cache:
tmp_var = old_var_cache[name]
tmp_var.unlink()
@api.depends('selectors', 'val_template', "vars")
def _compute_val(self):
'''
compute the val
:return:
'''
# check if mode has background image
for record in self:
# if every var is color and the color val is same as the ref var
background_image = record.theme_style.background_image
scss_var_cache = record.theme_mode.get_scss_var_record_cache()
opacity = record.theme_style.opacity
vars_cache = dict()
all_color = True
all_ref = True
all_same = True
for var in record.vars:
if var.type == 'color':
# check if has opacity
color = var.color
if var.ref_var and var.ref_var in scss_var_cache:
ref_color = scss_var_cache[var.ref_var]
if ref_color != color:
all_same = False
else:
all_ref = False
try:
if background_image:
if not var.disable_opacity:
color = apply_alpha(var.name, color, opacity, var.color_type)
except Exception as e:
_logger.error("apply alpha error {e}".format(e=e))
vars_cache[var.name] = color
elif var.type == 'image':
vars_cache[var.name] = var.image_file_url
all_color = False
elif var.type == 'image_url':
vars_cache[var.name] = var.image_url
all_color = False
else:
vars_cache[var.name] = var.svg
all_color = False
# if all the color are the original color, them ignore it
if all_color and all_ref and all_same:
record.val = False
else:
if not record.vars:
record.val = False
else:
try:
record.val = str(record.val_template).format(**vars_cache)
except Exception as error:
record.val = False
_logger.info('parse var value error, item name {item_name}->{error}'.format(
item_name=record['name'], error=error))
def get_style_item_data(self):
'''
get the style item data
:return:
'''
export_style = self.env.context.get('export_style', False)
style_items = []
for record in self:
style_item = record.read(
fields=["id", "name", "is_default", "val_template", "val", "selectors"])[0]
try:
style_item['selectors'] = json.loads(style_item['selectors'])
except Exception as error:
_logger.info('try to decode selectors error!', error)
if not export_style:
# maybe the image is very large, so do not read the image
style_item["vars"] = \
record.vars.read(
["name", "identity", "type", "image_file_url", "image_url", "color", "svg", "color_type"])
else:
style_item["vars"] = \
record.vars.read(
["name", "identity", "type", "image", "image_url", "color", "svg", "color_type"])
for tmp_var in style_item["vars"]:
if tmp_var["image"]:
tmp_var["image"] = str(tmp_var["image"])
style_items.append(style_item)
return style_items
def write(self, vals):
"""
rewrite to check if the selectors are right
:param vals:
:return:
"""
super(AnitaThemeStyleItem, self).write(vals)
vars_cache = dict()
for var in self.vars:
if var.type == 'color':
vars_cache[var.name] = var.color
elif var.type == 'image':
vars_cache[var.name] = var.image_file_url
elif var.type == 'image_url':
vars_cache[var.name] = var.image_url
else:
vars_cache[var.name] = var.svg
# check the var
try:
str(self.val_template).format(**vars_cache)
except Exception as error:
_logger.info('parse var value error, item name {item_name}->{error}'.format(
item_name=self['name'], error=error))
def get_login_style_data(
self, theme_style_id, login_style):
"""
get login style data
:return:
"""
result = []
theme_style = self.env["anita_theme_setting.theme_style"].browse(theme_style_id)
sub_group_ids = theme_style.mapped('groups.sub_groups.id')
style_items = self.search(
[('type', '=', 'login'),
('login_style', '=', login_style),
('sub_group', 'in', sub_group_ids)])
for style_item in style_items:
val = style_item.val
if val == "":
continue
if not style_item.selectors:
continue
try:
selector = ','.join(json.loads(style_item["selectors"]))
tmp_txt = selector + " {" + val + "}"
result.append(tmp_txt)
except Exception as error:
_logger.info(error)
return ";".join(result)
def delete_style_item(self):
'''
delete style item
:return:
'''
self.ensure_one()
self.unlink()
def create_style_item(
self, default_mode, sub_group, style_item, style_item_index):
'''
create group from default data
:return:
'''
selectors = json.dumps(style_item["selectors"])
var_infos = style_item["vars"]
var_val_cache = default_mode.get_var_val_cache()
var_vals = []
for var_index, var_info in enumerate(var_infos):
color = var_info.get("color", False)
ref_var = var_info.get("ref_var", False)
if ref_var:
if ref_var not in var_val_cache:
color = False
_logger.info("ref var not in var cache", ref_var)
else:
color = var_val_cache[ref_var]
var_vals.append((0, 0, {
"name": var_info["name"],
"type": var_info["type"],
"sequence": var_index,
"color": color,
"image": var_info.get('image', False),
"image_url": var_info.get('image_url', False),
"svg": var_info.get('svg', False),
"identity": var_info.get('identity', False),
"ref_var": ref_var
}))
style_item_val = {
"name": style_item["name"],
"sequence": style_item_index,
"val_template": style_item["val_template"],
"selectors": selectors,
"vars": var_vals,
"sub_group": sub_group
}
return self.create([style_item_val])
def resert_var(self):
"""
reset var, resut to the derfault value
:return:
"""
if not self.ref_var and self.type != 'color':
return False
# get mode first
var_cache = self.sub_group.group.theme_style.theme_mode.get_scss_vars_cache()
for var in self.vars.filtered(lambda x: x.type == 'color'):
if var.ref_var:
var.color = var_cache.get(var.ref_var, False)
return True
@@ -0,0 +1,199 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions, _, exceptions
import json
class AnitaStyleGroup(models.Model):
'''
user theme style group
'''
_name = 'anita_theme_setting.style_item_group'
_order = 'sequence asc'
_description = "style item group"
name = fields.Char(string="name", required=True)
is_default = fields.Boolean(string="is default", default=False)
theme_style = fields.Many2one(
string="theme style",
comodel_name="anita_theme_setting.theme_style",
ondelete="cascade")
sub_groups = fields.One2many(
string="sub groups",
comodel_name="anita_theme_setting.style_item_sub_group",
inverse_name="group")
sequence = fields.Integer(string="sequence", default=0)
def check_sub_group(self, default_mode, sub_groups):
'''
check group items, if not exsits create it else update it
need fixed
:return:0
'''
self.ensure_one()
old_sub_group_cache = {
sub_group.name: sub_group for sub_group in self.sub_groups}
default_sub_group_cache = {
default_group["name"]: default_group for default_group in sub_groups}
for sub_group_index, sub_group in enumerate(sub_groups):
if sub_group["name"] not in old_sub_group_cache:
self.env['anita_theme_setting.style_item_sub_group']\
.create_sub_group(default_mode, self.id, sub_group, sub_group_index)
else:
# update the style item
tmp_group = old_sub_group_cache[sub_group["name"]]
tmp_group.write({
"name": sub_group["name"],
"is_default": True,
"sequence": sub_group_index,
})
# check group item data
style_items = sub_group["style_items"]
tmp_group.check_style_items(default_mode, style_items)
# delete the style item (just not default, user may add item)
for sub_group_name in old_sub_group_cache:
if sub_group_name not in default_sub_group_cache \
and not old_sub_group_cache[sub_group_name].is_default:
old_sub_group_cache[sub_group_name].unlink()
def create_group_from_default_data(self, theme_style, group, group_index):
'''
create group from default data
:return:
'''
val = {
"name": group["name"],
"theme_style": theme_style.id,
"sequence": group_index,
}
mode = theme_style.theme_mode # get theme mode
scss_vars_cache = mode.get_scss_vars_cache()
sub_group_array = []
sub_groups = group["sub_groups"]
for sub_group_index, sub_group in enumerate(sub_groups):
style_item_vals = []
items = sub_group["style_items"]
for item_index, item in enumerate(items):
selectors = json.dumps(item["selectors"])
var_infos = item["vars"]
var_vals = []
for var_index, var_info in enumerate(var_infos):
ref_var = var_info.get('ref', False)
if ref_var:
if ref_var in scss_vars_cache:
raise exceptions.ValidationError(
_("The variable %s has been used") % ref_var)
else:
color = scss_vars_cache[ref_var]
else:
color = var_info.get('color', False)
var_vals.append((0, 0, {
"name": var_info["name"],
"type": var_info["type"],
"sequence": var_index,
"color": color,
"image": var_info.get('image', False),
"image_url": var_info.get('image_url', False),
"svg": var_info.get('svg', False),
"identity": var_info.get('identity', False)
}))
style_item_vals.append((0, 0, {
"name": item["name"],
"sequence": item_index,
"sub_group": sub_group["name"],
"selectors": selectors,
"vars": var_vals
}))
sub_group_array.append((0, 0, {
"name": sub_group["name"],
"sequence": sub_group_index,
"style_items": style_item_vals,
"is_default": True
}))
val["sub_groups"] = sub_group_array
return self.create([val])
@api.model
def get_add_new_group_action(self, style_id):
"""
get add new group action
:param style_id:
:return:
"""
theme_style = self.env["anita_theme_setting.theme_style"].browse(style_id)
old_groups = theme_style.groups
return {
"type": "ir.actions.act_window",
"res_model": "anita_theme_setting.style_item_group",
"view_mode": "form",
"target": "new",
"context": {
"default_theme_style": style_id,
"default_sequence": len(old_groups) + 1
},
"views": [[self.env.ref('anita_theme_setting.style_item_group_form').id, "form"]]
}
def get_group_data(self):
"""
get sub group data
:return:
"""
group_datas = []
for record in self:
group_datas.append({
"id": record.id,
"name": record.name,
"is_default": record.is_default,
"sequence": record.sequence,
"sub_groups": record.sub_groups.get_sub_group_data()
})
return group_datas
def get_add_sub_group_action(self):
"""
get add sub group action
:return:
"""
return {
"type": "ir.actions.act_window",
"res_model": "anita_theme_setting.style_item_sub_group",
"view_mode": 'form',
"target": "new",
"context": {
"default_group": self.id,
},
"views": [[self.env.ref('anita_theme_setting.style_item_sub_group_form').id, "form"]]
}
def delete_item_group(self):
"""
delete item group
:return:
"""
if len(self.sub_groups) > 0:
raise exceptions.ValidationError(
'there has some sub group left, please delete them first!')
self.unlink()
def get_sub_group_data(self):
"""
get sub group data
:return:
"""
sub_group_datas = []
for record in self:
sub_group_datas.append(record.get_sub_group_data())
return sub_group_datas
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, exceptions
import logging, json
_logger = logging.getLogger(__name__)
class AnitaStyleSubGroup(models.Model):
'''
user theme style group
'''
_name = 'anita_theme_setting.style_item_sub_group'
_order = 'sequence asc'
_description = "style item group"
name = fields.Char(string="name", required=True)
is_default = fields.Boolean(string="is default", default=False)
group = fields.Many2one(string="group",
comodel_name="anita_theme_setting.style_item_group",
ondelete="cascade")
sequence = fields.Integer(string="sequence", default=0)
style_items = fields.One2many(
string="style items",
comodel_name="anita_theme_setting.style_item",
inverse_name="sub_group")
def check_style_items(self, default_mode, style_items):
'''
check group items, if not exsits create it else update it
need fixed
:return:0
'''
self.ensure_one()
old_style_item_cache = {
style_item.name: style_item for style_item in self.style_items if style_item.is_default}
new_style_item_cache = {
style_item["name"]: style_item for style_item in style_items}
for item_index, style_item in enumerate(style_items):
if style_item["name"] not in old_style_item_cache:
self.env["anita_theme_setting.style_item"].create_style_item(
default_mode, self.id, style_item, item_index)
else:
old_style_item = old_style_item_cache[style_item["name"]]
old_style_item.write({
"selectors": json.dumps(style_item["selectors"]),
"name": style_item["name"],
"val_template": style_item["val_template"],
"sequence": item_index,
})
item_vars = style_item["vars"]
old_style_item.check_vars(default_mode, self.id, item_vars)
# delete the style item (just not default, user may add item)
for tmp_style_item in self.style_items:
if tmp_style_item.name not in new_style_item_cache:
old_style_item_cache[tmp_style_item.name].unlink()
def create_sub_group(self, default_mode, group, sub_group, sub_group_index):
'''
create sub group from default data
:return:
'''
val = {
"name": sub_group["name"],
"group": group,
"sequence": sub_group_index,
"is_default": True
}
var_val_cache = default_mode.get_var_val_cache()
style_item_vals = []
style_items = sub_group["style_items"]
for item_index, item in enumerate(style_items):
try:
selectors = json.dumps(item["selectors"])
except Exception as error:
_logger.info('parse selectors error:', error)
continue
var_infos = item["vars"]
var_vals = []
for var_index, var_info in enumerate(var_infos):
color = var_info.get("color", False)
ref_var = var_info.get("ref_var", False)
if ref_var:
if ref_var not in var_val_cache:
color = False
_logger.info('ref_var not in var_val_cache:', ref_var)
else:
color = var_val_cache[ref_var]
var_vals.append((0, 0, {
"name": var_info["name"],
"type": var_info["type"],
"sequence": var_index,
"color": color,
"image": var_info.get('image', False),
"image_url": var_info.get('image_url', False),
"svg": var_info.get('svg', False),
"identity": var_info.get('identity', False),
"ref_var": ref_var
}))
style_item_vals.append((0, 0, {
"name": item["name"],
"sequence": item_index,
"sub_group": sub_group["name"],
"selectors": selectors,
"vars": var_vals
}))
val["style_items"] = style_item_vals
return self.create([val])
def get_sub_group_data(self):
'''
get group data
:return:
'''
sub_groups = []
for record in self:
sub_groups.append({
"id": record.id,
"name": record.name,
"style_items": record.style_items.get_style_item_data(),
"sequence": record.sequence,
"is_default": record.is_default,
"expanded": False
})
return sub_groups
def delete_sub_group(self):
"""
delete sub group
:return:
"""
self.ensure_one()
if len(self.style_items) > 0:
raise exceptions.ValidationError(
'there has some style item left, please delete the style item first!')
self.unlink()
@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions
import datetime
from datetime import date, datetime
class AnitaThemeUsers(models.Model):
'''
user theme style setting
'''
_inherit = "res.users"
_description = 'user setting'
setting_id = fields.Many2one(
comodel_name="anita_theme_setting.setting_base",
ondelete="cascade",
string="settings")
@api.model
def save_theme_setting(self, settings):
'''
save settings, create if there is no user info
just use full when the mode is user wide mode
:param settings:
:return:
'''
self.ensure_setting_id()
self.setting_id.write(settings)
def ensure_setting_id(self):
'''
check setting id
:return:
'''
self.ensure_one()
if self.setting_id:
return
# copy use the global default data
values = self.env['res.config.settings'].get_theme_values()
# get fields of anita_theme_setting.setting_base
field_names = [name for name, field in self.setting_id._fields.items() if name not in [
'id', 'create_uid', 'create_date', 'write_uid', 'write_date']]
# remove name not in fields
values = {k: v for k, v in values.items() if k in field_names}
del values["current_theme_mode"]
del values["current_theme_style"]
if not self.setting_id:
tmp_record = self.env["anita_theme_setting.setting_base"].create(values)
self.setting_id = tmp_record.id
def get_theme_setting(self):
'''
get settings
:return:
'''
record = self.env.user
record.ensure_setting_id()
field_names = [name for name, field in record.setting_id._fields.items() if name not in [
"icon128x128", "icon144x144", "icon152x152", "icon192x192", "icon256x256", "icon512x512"]]
result = record.setting_id.read(fields=field_names)[0]
result["current_theme_mode"] = record.setting_id.current_theme_mode.id
result["current_theme_style"] = record.setting_id.current_theme_style.id
for key, item in result.items():
if isinstance(item, datetime):
result[key] = fields.Datetime.to_string(item)
if isinstance(item, date):
result[key] = fields.Date.to_string(item)
return result
def check_default_data(self):
"""
check default data
:return:
"""
# check mode data
owner = 'res.users, {user_id}'.format(user_id=self.id)
self.env["anita_theme_setting.theme_mode"].check_default_mode_data(owner)
@api.model
def get_user_xml_groups(self):
'''
:return:
'''
group_ids = self.env.user.groups_id.ids
records = self.env['ir.model.data']\
.sudo() \
.search_read([('model', '=', 'res.groups'),
('res_id', 'in', group_ids)], fields=["complete_name"])
return {record["complete_name"]: True for record in records}
def get_mode_domain(self):
"""
get mode domain dynamic
:return:
"""
owner = 'res.users, {user_id}'.format(user_id=self.id)
return [('owner', '=', owner)]
@api.model
def get_group_infos(self):
'''
:return:
'''
group_ids = self.env.user.groups_id.ids
records = self.env['ir.model.data']\
.sudo() \
.search_read(
[('model', '=', 'res.groups'),
('res_id', 'in', group_ids)],
fields=["complete_name"])
groups = {record["complete_name"]: True for record in records}
return {
"groups": groups,
"group_ids": group_ids,
"user_id": self.env.user.id
}
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class AnitaThemeVar(models.Model):
'''
user theme style setting
'''
_name = 'anita_theme_setting.theme_var'
_order = 'sequence asc'
_description = "Theme Var"
sequence = fields.Integer(string="sequence", default=0)
style_item = fields.Many2one(
comodel_name="anita_theme_setting.style_item",
string="style_item",
required=True,
ondelete="cascade")
type = fields.Selection(
selection=[('color', 'color'),
('image', 'image'),
('image_url', 'image_url'),
('svg', 'svg')],
string="type",
default="color")
name = fields.Char(string="Name", default="var1")
color = fields.Char(string="Val")
color_type = fields.Selection(
selection=[
('background', 'background'),
('text', 'text'),
('border', 'border'),
('shadow', 'shadow')],
default='background', string="color type", help="Type use to apply aphla!")
disable_opacity = fields.Boolean(string="Disable Opacity", default=False)
image = fields.Binary(string="image", attachment=True)
image_file_url = fields.Char(string="Image File Url", compute="_compute_image_file_url")
image_url = fields.Char(string="Image Url")
svg = fields.Text(string="Svg")
ref_var = fields.Char(string="Ref Var Name")
opacity = fields.Float(string="Opacity", default=1.0)
identity = fields.Char(string="identify")
is_default = fields.Boolean(string="Is Default", default=False)
@api.depends('image')
def _compute_image_file_url(self):
"""
compute image file url
:return:
"""
for record in self:
if record.type == 'image' and record.image:
record.image_file_url = \
'/web/image/anita_theme_setting.theme_var/{var_id}/image'.format(
var_id=record.id)
else:
record.image_file_url = False
def get_edit_var_action(self):
"""
get edit action
:return:
"""
return {
"type": "ir.actions.act_window",
"res_model": "anita_theme_setting.theme_var",
"res_id": self.id,
"view_mode": "form",
"target": "new",
"views": [[self.env.ref(
'anita_theme_setting.edit_theme_var_form').id, "form"]]
}
def get_var_data(self):
"""
get var data
:return:
"""
return self.read(
fields=["name", "identity", "type",
"image_file_url", "image_url", "color", "svg"])
def reset_var(self):
"""
reset var
:return:
"""
if not self.ref_var and self.type != 'color':
return False
# get mode first
var_cache = self.style_item.sub_group.group.theme_style.theme_mode.get_scss_vars_cache()
if self.ref_var in var_cache:
self.color = var_cache.get(self.ref_var, False)
return True
+132
View File
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
from math import fabs
from odoo import exceptions
def apply_alpha(var_name, color, alpha, color_type = False):
"""
safe alpha
:param color:
:param alpha:
:return:
"""
if not color:
return ''
color.strip()
# check var_name endwith text_color
if var_name.endswith("text_color") or color_type == "text_color":
return color
# replace white to fff and black to 000 etc
if color == "white":
color = "fff"
elif color == "black":
color = "000"
elif color == "red":
color = "f00"
elif color == "green":
color = "0f0"
elif color == "blue":
color = "00f"
elif color == "yellow":
color = "ff0"
# check if start with rgba
if color.startswith("rgba"):
# get the content between ( and )
color = color[color.find("(") + 1:color.find(")")]
# get the alpha
rgba_color = color.split(",")
if len(rgba_color) == 4:
# convert to float
tmp = float(rgba_color[3])
# check var_name has shadow in it
tmp = alpha * tmp
# make rgba string
tmp = "rgba({rgba_color[0]},{rgba_color[1]},{rgba_color[2]},{tmp})".format(
rgba_color=rgba_color,
tmp=tmp)
return tmp
else:
return color
# check if start with # and length is 9
elif color.startswith("#"):
if len(color) == 9 or len(color) == 7:
# get red color
red = color[1:3]
# get green color
green = color[3:5]
# get blue color
blue = color[5:7]
# get the old alpha
if len(color) == 9:
old_alpha = color[7:9]
# convert to int
old_alpha = int(old_alpha, 16) / 255.0
new_alpha = int(old_alpha * alpha)
else:
new_alpha = alpha
# make rgba string
return "rgba({red},{green},{blue},{new_alpha})".format(
red=int(red, 16),
green=int(green, 16),
blue=int(blue, 16),
new_alpha=new_alpha)
# color like #ffff
elif len(color) == 5:
# get red color
red = color[1:2]
# get green color
green = color[2:3]
# get blue color
blue = color[3:4]
# alpha
old_alpha = color[4:5]
# convert to int
old_alpha = int(old_alpha + old_alpha, 16) / 255.0
new_alpha = old_alpha * alpha
# make rgba string
return "rgba({red},{green},{blue},{alpha})".format(
red=int(red + red, 16),
green=int(green + green, 16),
blue=int(blue + blue, 16),
alpha=new_alpha)
# color like #fff
elif len(color) == 4:
# get red color
red = color[1:2]
# get green color
green = color[2:3]
# get blue color
blue = color[3:4]
# make rgba string
return "rgba({red},{green},{blue},{alpha})".format(
red=int(red + red, 16),
green=int(green +green, 16),
blue=int(blue + blue, 16),
alpha=alpha)
else:
return color
else:
# check if start with rgb
if color.startswith("rgb"):
# get the content between ( and )
color = color[color.find("(") + 1:color.find(")")]
# get the alpha
rgb_color = color.split(",")
if len(rgb_color) == 3:
# make rgba string
tmp = "rgba({rgb_color[0]},{rgb_color[1]},{rgb_color[2]},{alpha})".format(
rgb_color=rgb_color,
alpha=alpha)
return tmp
else:
return color
else:
return 'rgba({color},{alpha})'.format(color=color, alpha=alpha)
@@ -0,0 +1,16 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
anita_theme_setting_style_item,anita_theme_setting.style_item,model_anita_theme_setting_style_item,,1,1,1,1
anita_theme_setting_theme_style,anita_theme_setting.theme_style,model_anita_theme_setting_theme_style,,1,1,1,1
anita_theme_setting_theme_mode,anita_theme_setting.theme_mode,model_anita_theme_setting_theme_mode,,1,1,1,1
anita_theme_setting_style_item_group,anita_theme_setting.style_item_group,model_anita_theme_setting_style_item_group,,1,1,1,1
anita_theme_setting_style_item_sub_group,anita_theme_setting.style_item_sub_group,model_anita_theme_setting_style_item_sub_group,,1,1,1,1
anita_theme_setting_setting_base,anita_theme_setting.setting_base,model_anita_theme_setting_setting_base,,1,1,1,1
anita_theme_setting_theme_var,anita_theme_setting.theme_var,model_anita_theme_setting_theme_var,,1,1,1,1
anita_theme_setting_setting_manager,anita_theme_setting.setting_manager,model_anita_theme_setting_setting_manager,,1,1,1,1
anita_theme_setting_default_mode_data,anita_theme_setting.default_mode_data,model_anita_theme_setting_default_mode_data,,1,1,1,1
anita_theme_setting_mode_template,anita_theme_setting.mode_template,model_anita_theme_setting_mode_template,,1,1,1,1
anita_theme_setting_import_theme_style,anita_theme_setting_import_theme_style,model_anita_theme_setting_import_theme_style,,1,1,1,1
anita_theme_setting_template_var_val,anita_theme_setting.template_var_val,model_anita_theme_setting_template_var_val,,1,1,1,1
access_anita_theme_setting_template_var_val,anita_theme_setting.template_var_val,model_anita_theme_setting_template_var_val,,1,1,1,1
access_anita_theme_setting_background_image_wizard,anita_theme_setting.background_image_wizard,model_anita_theme_setting_background_image_wizard,,1,1,1,1
access_anita_theme_setting_favorite_menu',anita_theme_setting.favorite_menu,model_anita_theme_setting_favorite_menu,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 anita_theme_setting_style_item anita_theme_setting.style_item model_anita_theme_setting_style_item 1 1 1 1
3 anita_theme_setting_theme_style anita_theme_setting.theme_style model_anita_theme_setting_theme_style 1 1 1 1
4 anita_theme_setting_theme_mode anita_theme_setting.theme_mode model_anita_theme_setting_theme_mode 1 1 1 1
5 anita_theme_setting_style_item_group anita_theme_setting.style_item_group model_anita_theme_setting_style_item_group 1 1 1 1
6 anita_theme_setting_style_item_sub_group anita_theme_setting.style_item_sub_group model_anita_theme_setting_style_item_sub_group 1 1 1 1
7 anita_theme_setting_setting_base anita_theme_setting.setting_base model_anita_theme_setting_setting_base 1 1 1 1
8 anita_theme_setting_theme_var anita_theme_setting.theme_var model_anita_theme_setting_theme_var 1 1 1 1
9 anita_theme_setting_setting_manager anita_theme_setting.setting_manager model_anita_theme_setting_setting_manager 1 1 1 1
10 anita_theme_setting_default_mode_data anita_theme_setting.default_mode_data model_anita_theme_setting_default_mode_data 1 1 1 1
11 anita_theme_setting_mode_template anita_theme_setting.mode_template model_anita_theme_setting_mode_template 1 1 1 1
12 anita_theme_setting_import_theme_style anita_theme_setting_import_theme_style model_anita_theme_setting_import_theme_style 1 1 1 1
13 anita_theme_setting_template_var_val anita_theme_setting.template_var_val model_anita_theme_setting_template_var_val 1 1 1 1
14 access_anita_theme_setting_template_var_val anita_theme_setting.template_var_val model_anita_theme_setting_template_var_val 1 1 1 1
15 access_anita_theme_setting_background_image_wizard anita_theme_setting.background_image_wizard model_anita_theme_setting_background_image_wizard 1 1 1 1
16 access_anita_theme_setting_favorite_menu' anita_theme_setting.favorite_menu model_anita_theme_setting_favorite_menu 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 MiB

Some files were not shown because too many files have changed in this diff Show More