new modles
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#################################################################################
|
||||||
|
# Author : Terabits Technolab (<www.terabits.xyz>)
|
||||||
|
# Copyright(c): 2021-22
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# This module is copyright property of the author mentioned above.
|
||||||
|
# You can`t redistribute it and/or modify it.
|
||||||
|
#
|
||||||
|
#################################################################################
|
||||||
|
{
|
||||||
|
'name': 'Advanced Web Domain Widget',
|
||||||
|
'version': '18.0.3.0.0',
|
||||||
|
'summary': 'Set all relational fields domain by selecting its records unsing `in, not in` operator.',
|
||||||
|
'sequence': 1,
|
||||||
|
'author': 'Terabits Technolab',
|
||||||
|
'license': 'OPL-1',
|
||||||
|
'website': 'https://www.terabits.xyz/apps/18.0/advanced_web_domain_widget',
|
||||||
|
'description':"""
|
||||||
|
|
||||||
|
""",
|
||||||
|
"price": "29.00",
|
||||||
|
"currency": "USD",
|
||||||
|
'depends':['base','web'],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'advanced_web_domain_widget/static/src/scss/style.scss',
|
||||||
|
'advanced_web_domain_widget/static/src/components/domain_selector/domain_selector.js',
|
||||||
|
'advanced_web_domain_widget/static/src/xml/domain_templates.xml',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'images': ['static/description/banner.png'],
|
||||||
|
'application': True,
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import domain_prepare
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,497 @@
|
||||||
|
from odoo.http import request
|
||||||
|
from datetime import datetime,timedelta
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
def prepare_domain_v2(domain):
|
||||||
|
if isinstance(domain, tuple) or isinstance(domain, list):
|
||||||
|
field_name = domain[0]
|
||||||
|
operator = domain[1]
|
||||||
|
val = domain[2]
|
||||||
|
|
||||||
|
date_format = '%Y-%m-%d %H:%M:%S'
|
||||||
|
|
||||||
|
current_date = datetime.now()
|
||||||
|
current_date = current_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
|
if operator != "date_filter":
|
||||||
|
return [tuple(domain)]
|
||||||
|
|
||||||
|
if val == "today":
|
||||||
|
start_of_today = current_date
|
||||||
|
end_of_today = current_date + timedelta(days=1)
|
||||||
|
return ["&", (field_name, ">=", start_of_today), (field_name, "<", end_of_today)]
|
||||||
|
|
||||||
|
if val == "this_week":
|
||||||
|
start_of_week = current_date - timedelta(days=current_date.weekday())
|
||||||
|
end_of_week = (current_date + timedelta(days=(7 - current_date.weekday())))
|
||||||
|
return ["&", (field_name, ">=", start_of_week), (field_name, "<", end_of_week)]
|
||||||
|
|
||||||
|
if val == "this_month":
|
||||||
|
start_of_month = current_date.replace(day=1)
|
||||||
|
end_of_month = current_date + relativedelta(day=31)
|
||||||
|
return ["&", (field_name, ">=", start_of_month), (field_name, "<=", end_of_month)]
|
||||||
|
|
||||||
|
if val == "this_quarter":
|
||||||
|
start_of_quarter = datetime(current_date.year, ((current_date.month - 1) // 3) * 3 + 1, 1)
|
||||||
|
end_of_quarter = start_of_quarter + relativedelta(months=3)
|
||||||
|
return ["&", (field_name, '>=', start_of_quarter), (field_name, '<', end_of_quarter)]
|
||||||
|
|
||||||
|
if val == "this_year":
|
||||||
|
start_of_year = current_date.replace(month=1, day=1)
|
||||||
|
end_of_year = start_of_year + relativedelta(years=1)
|
||||||
|
return ["&", (field_name, ">=", start_of_year.strftime(date_format)), (field_name, "<", end_of_year.strftime(date_format))]
|
||||||
|
|
||||||
|
if val == "last_day":
|
||||||
|
start_of_yesterday = current_date - timedelta(days=1)
|
||||||
|
return ["&", (field_name, ">=", start_of_yesterday), (field_name, "<", current_date)]
|
||||||
|
|
||||||
|
if val == "last_week":
|
||||||
|
end_of_last_week = current_date - timedelta(days=current_date.weekday())
|
||||||
|
start_of_last_week = end_of_last_week - timedelta(days=6)
|
||||||
|
return ["&", (field_name, ">=", start_of_last_week), (field_name, "<", end_of_last_week)]
|
||||||
|
|
||||||
|
if val == "last_month":
|
||||||
|
start_of_last_month = (current_date - relativedelta(months=1)).replace(day=1)
|
||||||
|
end_of_last_month = start_of_last_month + relativedelta(months=1)
|
||||||
|
return ["&", (field_name, ">=", start_of_last_month), (field_name, "<", end_of_last_month)]
|
||||||
|
|
||||||
|
if val == "last_quarter":
|
||||||
|
start_of_this_quarter = datetime(current_date.year, ((current_date.month - 1) // 3) * 3 + 1, 1)
|
||||||
|
end_of_last_quarter = start_of_this_quarter
|
||||||
|
start_of_last_quarter = (end_of_last_quarter - relativedelta(months=3)).replace(day=1)
|
||||||
|
return ["&", (field_name, ">=", start_of_last_quarter), (field_name, "<", end_of_last_quarter)]
|
||||||
|
|
||||||
|
if val == "last_year":
|
||||||
|
end_of_last_year = datetime(current_date.year-1, 1, 1)
|
||||||
|
start_of_last_year = datetime(current_date.year, 1, 1)
|
||||||
|
return ["&", (field_name, ">=", start_of_last_year), (field_name, "<", end_of_last_year)]
|
||||||
|
|
||||||
|
if val == "last_7_days":
|
||||||
|
start_of_last_7_days = current_date - timedelta(days=6)
|
||||||
|
return [(field_name, ">=", start_of_last_7_days)]
|
||||||
|
|
||||||
|
if val == "last_30_days":
|
||||||
|
start_of_last_30_days = current_date - timedelta(days=29)
|
||||||
|
return [(field_name, ">=", start_of_last_30_days)]
|
||||||
|
|
||||||
|
if val == "last_90_days":
|
||||||
|
start_of_last_90_days = current_date - timedelta(days=89)
|
||||||
|
return [(field_name, ">=", start_of_last_90_days)]
|
||||||
|
|
||||||
|
if val == "last_365_days":
|
||||||
|
start_of_last_365_days = current_date - timedelta(days=364)
|
||||||
|
return [(field_name, ">=", start_of_last_365_days)]
|
||||||
|
|
||||||
|
if val == "next_day":
|
||||||
|
start_of_next_day = current_date + timedelta(days=1)
|
||||||
|
end_of_next_day = start_of_next_day + timedelta(days=1)
|
||||||
|
return ["&", (field_name, ">=", start_of_next_day), (field_name, "<", end_of_next_day)]
|
||||||
|
|
||||||
|
if val == "next_week":
|
||||||
|
start_of_next_week = current_date + timedelta(days=(7 - current_date.weekday()))
|
||||||
|
end_of_next_week = start_of_next_week + timedelta(days=7)
|
||||||
|
return ["&", (field_name, ">=", start_of_next_week), (field_name, "<", end_of_next_week)]
|
||||||
|
|
||||||
|
if val == "next_month":
|
||||||
|
start_of_next_month = (current_date + relativedelta(months=1)).replace(day=1)
|
||||||
|
end_of_next_month = (start_of_next_month + relativedelta(months=1)).replace(day=1)
|
||||||
|
return ["&", (field_name, ">=", start_of_next_month), (field_name, "<", end_of_next_month)]
|
||||||
|
|
||||||
|
if val == "next_quarter":
|
||||||
|
end_of_quarter = datetime(current_date.year, (((current_date.month - 1) // 3) * 3 + 3)+1, 1)
|
||||||
|
start_of_next_quarter = end_of_quarter
|
||||||
|
end_of_next_quarter = (start_of_next_quarter + relativedelta(months=3)).replace(day=1)
|
||||||
|
return ["&", (field_name, ">=", start_of_next_quarter), (field_name, "<", end_of_next_quarter)]
|
||||||
|
|
||||||
|
if val == "next_year":
|
||||||
|
start_of_next_year = datetime(current_date.year+1, 1, 1)
|
||||||
|
end_of_next_year = datetime(current_date.year+2, 1, 1)
|
||||||
|
return ["&", (field_name, ">=", start_of_next_year), (field_name, "<", end_of_next_year)]
|
||||||
|
|
||||||
|
return [tuple(domain)]
|
||||||
|
|
||||||
|
# def prepare_domain(domain):
|
||||||
|
# prepared_domain =[]
|
||||||
|
# if isinstance(domain, tuple) or isinstance(domain, list):
|
||||||
|
# left_value = domain[0]
|
||||||
|
# operator_value = domain[1]
|
||||||
|
# right_value = domain[2]
|
||||||
|
# if operator_value == 'date_filter':
|
||||||
|
|
||||||
|
# current_date=datetime.now()
|
||||||
|
# dom_list=list(domain)
|
||||||
|
|
||||||
|
# if right_value == 'today':
|
||||||
|
|
||||||
|
# dom_list[1]='='
|
||||||
|
# dom_list[2]=current_date
|
||||||
|
|
||||||
|
# elif right_value == 'this_week':
|
||||||
|
# dom_list[1]='>'
|
||||||
|
# current_day_of_week = current_date.weekday()
|
||||||
|
# if current_day_of_week == 6:
|
||||||
|
# first_date_of_week = current_date
|
||||||
|
# else:
|
||||||
|
# days_until_start_of_week = current_day_of_week + 1
|
||||||
|
# first_date_of_week = current_date - timedelta(days=days_until_start_of_week)
|
||||||
|
|
||||||
|
# dom_list[2]=first_date_of_week
|
||||||
|
|
||||||
|
# elif right_value == 'this_month':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# first_date_of_month=current_date.replace(day=1)
|
||||||
|
# dom_list[2]=first_date_of_month
|
||||||
|
|
||||||
|
# elif right_value == 'this_quarter':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# current_month = current_date.month
|
||||||
|
# current_quarter = (current_month - 1) // 3 + 1
|
||||||
|
# current_year = current_date.year
|
||||||
|
# quarter_start_month = (current_quarter - 1) * 3 + 1
|
||||||
|
# quarter_start_date = datetime(current_year, quarter_start_month, 1)
|
||||||
|
|
||||||
|
# quarter_end_month = quarter_start_month + 2
|
||||||
|
# quarter_end_date = datetime(current_year, quarter_end_month, 1)
|
||||||
|
# quarter_end_date = quarter_end_date.replace(day=quarter_end_date.day, hour=23, minute=59, second=59)
|
||||||
|
|
||||||
|
# dom_list[2]=quarter_start_date
|
||||||
|
|
||||||
|
# this_qua_end_dom_list.append(dom_list[0])
|
||||||
|
# this_qua_end_dom_list.append('<=')
|
||||||
|
# this_qua_end_dom_list.append(quarter_end_date)
|
||||||
|
|
||||||
|
# elif right_value == 'this_year':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# first_this_year_date=datetime(current_date.year, 1, 1)
|
||||||
|
# last_date_of_year = datetime(current_date.year, 12, 31)
|
||||||
|
# dom_list[2]=first_this_year_date
|
||||||
|
|
||||||
|
# this_year_end_dom_list.append(dom_list[0])
|
||||||
|
# this_year_end_dom_list.append('<=')
|
||||||
|
# this_year_end_dom_list.append(last_date_of_year)
|
||||||
|
|
||||||
|
|
||||||
|
# elif right_value == 'last_day':
|
||||||
|
|
||||||
|
# dom_list[1]='>'
|
||||||
|
# last_day_date=current_date+ timedelta(days=-1)
|
||||||
|
# dom_list[2]=last_day_date
|
||||||
|
|
||||||
|
# elif right_value == 'last_week':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
|
||||||
|
# last_week_start = current_date - timedelta(days=current_date.weekday() + 8)
|
||||||
|
# last_week_end = last_week_start + timedelta(days=6)
|
||||||
|
# dom_list[2]=last_week_start
|
||||||
|
|
||||||
|
# last_week_end_date_dom_list.append(dom_list[0])
|
||||||
|
# last_week_end_date_dom_list.append('<=')
|
||||||
|
# last_week_end_date_dom_list.append(last_week_end)
|
||||||
|
|
||||||
|
# elif right_value == 'last_month':
|
||||||
|
|
||||||
|
# dom_list[1]='>='
|
||||||
|
|
||||||
|
# last_month_end_date=current_date.replace(day=1)- timedelta(days=1)
|
||||||
|
# if last_month_end_date.strftime('%d')=='31':
|
||||||
|
# last_month_start_date=last_month_end_date-timedelta(days=30)
|
||||||
|
|
||||||
|
# elif last_month_end_date.strftime('%d')=='30':
|
||||||
|
# last_month_start_date=last_month_end_date-timedelta(days=29)
|
||||||
|
|
||||||
|
# elif last_month_end_date.strftime('%d')=='29':
|
||||||
|
# last_month_start_date=last_month_end_date-timedelta(days=28)
|
||||||
|
|
||||||
|
# elif last_month_end_date.strftime('%d')=='28':
|
||||||
|
# last_month_start_date=last_month_end_date-timedelta(days=27)
|
||||||
|
|
||||||
|
# dom_list[2]=last_month_start_date
|
||||||
|
|
||||||
|
# last_month_end_date_dom_list.append(dom_list[0])
|
||||||
|
# last_month_end_date_dom_list.append('<=')
|
||||||
|
# last_month_end_date_dom_list.append(last_month_end_date)
|
||||||
|
|
||||||
|
# elif right_value == 'last_quarter':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# current_month = current_date.month
|
||||||
|
# current_quarter = (current_month - 1) // 3 + 1
|
||||||
|
# current_year = current_date.year
|
||||||
|
|
||||||
|
# if current_quarter == 1:
|
||||||
|
# last_quarter_start = datetime(current_year - 1, 10, 1)
|
||||||
|
# else:
|
||||||
|
# last_quarter_start = datetime(current_year, (current_quarter - 2) * 3 + 1, 1)
|
||||||
|
|
||||||
|
# if current_quarter == 1:
|
||||||
|
# last_quarter_end = datetime(current_year - 1, 12, 31)
|
||||||
|
# else:
|
||||||
|
# last_quarter_end = datetime(current_year, (current_quarter - 1) * 3, 1) - timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
|
# dom_list[2]=last_quarter_start
|
||||||
|
|
||||||
|
# last_qua_end_dom_list.append(dom_list[0])
|
||||||
|
# last_qua_end_dom_list.append('<=')
|
||||||
|
# last_qua_end_dom_list.append(last_quarter_end)
|
||||||
|
|
||||||
|
# elif right_value == 'last_year':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_year = current_date.year - 1
|
||||||
|
# first_date_of_last_year = datetime(last_year, 1, 1)
|
||||||
|
# last_date_of_last_year = datetime(last_year, 12, 31)
|
||||||
|
# dom_list[2]=first_date_of_last_year
|
||||||
|
|
||||||
|
# last_year_end_dom_list.append(dom_list[0])
|
||||||
|
# last_year_end_dom_list.append('<=')
|
||||||
|
# last_year_end_dom_list.append(last_date_of_last_year)
|
||||||
|
|
||||||
|
# elif right_value == 'last_7_days':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_7_days_date=current_date+ timedelta(days=-7)
|
||||||
|
# dom_list[2]=last_7_days_date
|
||||||
|
|
||||||
|
# elif right_value == 'last_30_days':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_30_days_date=current_date+ timedelta(days=-30)
|
||||||
|
# dom_list[2]=last_30_days_date
|
||||||
|
|
||||||
|
# elif right_value == 'last_90_days':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_90_days_date=current_date+ timedelta(days=-90)
|
||||||
|
# dom_list[2]=last_90_days_date
|
||||||
|
|
||||||
|
# elif right_value == 'last_365_days':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_365_days_date=current_date+ timedelta(days=-365)
|
||||||
|
# dom_list[2]=last_365_days_date
|
||||||
|
|
||||||
|
# elif right_value == "next_day":
|
||||||
|
# next_day_date=current_date+ timedelta(days=+1)
|
||||||
|
# dom_list[1]='>'
|
||||||
|
# last_365_days_date=current_date+ timedelta(days=-365)
|
||||||
|
# dom_list[2]=last_365_days_date
|
||||||
|
# pass
|
||||||
|
# elif right_value == "next_week":
|
||||||
|
# pass
|
||||||
|
# elif right_value == "next_month":
|
||||||
|
# pass
|
||||||
|
# elif right_value == "next_year":
|
||||||
|
# pass
|
||||||
|
|
||||||
|
|
||||||
|
# else:
|
||||||
|
# prepared_domain.append(dom_tuple)
|
||||||
|
# return prepared_domain
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# def prepare_domain(domain):
|
||||||
|
# prepared_domain =[]
|
||||||
|
# if isinstance(domain, tuple) or isinstance(domain, list):
|
||||||
|
# left_value = domain[0]
|
||||||
|
# operator_value = domain[1]
|
||||||
|
# right_value = domain[2]
|
||||||
|
# if operator_value == 'date_filter':
|
||||||
|
|
||||||
|
# current_date=datetime.now()
|
||||||
|
# dom_list=list(domain)
|
||||||
|
# today_end_time=[]
|
||||||
|
# this_qua_end_dom_list=[]
|
||||||
|
# last_qua_end_dom_list=[]
|
||||||
|
# this_year_end_dom_list=[]
|
||||||
|
# last_year_end_dom_list=[]
|
||||||
|
# last_week_end_date_dom_list=[]
|
||||||
|
# last_month_end_date_dom_list=[]
|
||||||
|
|
||||||
|
# if right_value == 'today':
|
||||||
|
|
||||||
|
# dom_list[1]='='
|
||||||
|
# dom_list[2]=current_date
|
||||||
|
|
||||||
|
# elif right_value == 'this_week':
|
||||||
|
# dom_list[1]='>'
|
||||||
|
# current_day_of_week = current_date.weekday()
|
||||||
|
# if current_day_of_week == 6:
|
||||||
|
# first_date_of_week = current_date
|
||||||
|
# else:
|
||||||
|
# days_until_start_of_week = current_day_of_week + 1
|
||||||
|
# first_date_of_week = current_date - timedelta(days=days_until_start_of_week)
|
||||||
|
|
||||||
|
# dom_list[2]=first_date_of_week
|
||||||
|
|
||||||
|
# elif right_value == 'this_month':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# first_date_of_month=current_date.replace(day=1)
|
||||||
|
# dom_list[2]=first_date_of_month
|
||||||
|
|
||||||
|
# elif right_value == 'this_quarter':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# current_month = current_date.month
|
||||||
|
# current_quarter = (current_month - 1) // 3 + 1
|
||||||
|
# current_year = current_date.year
|
||||||
|
# quarter_start_month = (current_quarter - 1) * 3 + 1
|
||||||
|
# quarter_start_date = datetime(current_year, quarter_start_month, 1)
|
||||||
|
|
||||||
|
# quarter_end_month = quarter_start_month + 2
|
||||||
|
# quarter_end_date = datetime(current_year, quarter_end_month, 1)
|
||||||
|
# quarter_end_date = quarter_end_date.replace(day=quarter_end_date.day, hour=23, minute=59, second=59)
|
||||||
|
|
||||||
|
# dom_list[2]=quarter_start_date
|
||||||
|
|
||||||
|
# this_qua_end_dom_list.append(dom_list[0])
|
||||||
|
# this_qua_end_dom_list.append('<=')
|
||||||
|
# this_qua_end_dom_list.append(quarter_end_date)
|
||||||
|
|
||||||
|
# elif right_value == 'this_year':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# first_this_year_date=datetime(current_date.year, 1, 1)
|
||||||
|
# last_date_of_year = datetime(current_date.year, 12, 31)
|
||||||
|
# dom_list[2]=first_this_year_date
|
||||||
|
|
||||||
|
# this_year_end_dom_list.append(dom_list[0])
|
||||||
|
# this_year_end_dom_list.append('<=')
|
||||||
|
# this_year_end_dom_list.append(last_date_of_year)
|
||||||
|
|
||||||
|
|
||||||
|
# elif right_value == 'last_day':
|
||||||
|
|
||||||
|
# dom_list[1]='>'
|
||||||
|
# last_day_date=current_date+ timedelta(days=-1)
|
||||||
|
# dom_list[2]=last_day_date
|
||||||
|
|
||||||
|
# elif right_value == 'last_week':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
|
||||||
|
# last_week_start = current_date - timedelta(days=current_date.weekday() + 8)
|
||||||
|
# last_week_end = last_week_start + timedelta(days=6)
|
||||||
|
# dom_list[2]=last_week_start
|
||||||
|
|
||||||
|
# last_week_end_date_dom_list.append(dom_list[0])
|
||||||
|
# last_week_end_date_dom_list.append('<=')
|
||||||
|
# last_week_end_date_dom_list.append(last_week_end)
|
||||||
|
|
||||||
|
# elif right_value == 'last_month':
|
||||||
|
|
||||||
|
# dom_list[1]='>='
|
||||||
|
|
||||||
|
# last_month_end_date=current_date.replace(day=1)- timedelta(days=1)
|
||||||
|
# if last_month_end_date.strftime('%d')=='31':
|
||||||
|
# last_month_start_date=last_month_end_date-timedelta(days=30)
|
||||||
|
|
||||||
|
# elif last_month_end_date.strftime('%d')=='30':
|
||||||
|
# last_month_start_date=last_month_end_date-timedelta(days=29)
|
||||||
|
|
||||||
|
# elif last_month_end_date.strftime('%d')=='29':
|
||||||
|
# last_month_start_date=last_month_end_date-timedelta(days=28)
|
||||||
|
|
||||||
|
# elif last_month_end_date.strftime('%d')=='28':
|
||||||
|
# last_month_start_date=last_month_end_date-timedelta(days=27)
|
||||||
|
|
||||||
|
# dom_list[2]=last_month_start_date
|
||||||
|
|
||||||
|
# last_month_end_date_dom_list.append(dom_list[0])
|
||||||
|
# last_month_end_date_dom_list.append('<=')
|
||||||
|
# last_month_end_date_dom_list.append(last_month_end_date)
|
||||||
|
|
||||||
|
# elif right_value == 'last_quarter':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# current_month = current_date.month
|
||||||
|
# current_quarter = (current_month - 1) // 3 + 1
|
||||||
|
# current_year = current_date.year
|
||||||
|
|
||||||
|
# if current_quarter == 1:
|
||||||
|
# last_quarter_start = datetime(current_year - 1, 10, 1)
|
||||||
|
# else:
|
||||||
|
# last_quarter_start = datetime(current_year, (current_quarter - 2) * 3 + 1, 1)
|
||||||
|
|
||||||
|
# if current_quarter == 1:
|
||||||
|
# last_quarter_end = datetime(current_year - 1, 12, 31)
|
||||||
|
# else:
|
||||||
|
# last_quarter_end = datetime(current_year, (current_quarter - 1) * 3, 1) - timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
|
# dom_list[2]=last_quarter_start
|
||||||
|
|
||||||
|
# last_qua_end_dom_list.append(dom_list[0])
|
||||||
|
# last_qua_end_dom_list.append('<=')
|
||||||
|
# last_qua_end_dom_list.append(last_quarter_end)
|
||||||
|
|
||||||
|
# elif right_value == 'last_year':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_year = current_date.year - 1
|
||||||
|
# first_date_of_last_year = datetime(last_year, 1, 1)
|
||||||
|
# last_date_of_last_year = datetime(last_year, 12, 31)
|
||||||
|
# dom_list[2]=first_date_of_last_year
|
||||||
|
|
||||||
|
# last_year_end_dom_list.append(dom_list[0])
|
||||||
|
# last_year_end_dom_list.append('<=')
|
||||||
|
# last_year_end_dom_list.append(last_date_of_last_year)
|
||||||
|
|
||||||
|
# elif right_value == 'last_7_days':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_7_days_date=current_date+ timedelta(days=-7)
|
||||||
|
# dom_list[2]=last_7_days_date
|
||||||
|
|
||||||
|
# elif right_value == 'last_30_days':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_30_days_date=current_date+ timedelta(days=-30)
|
||||||
|
# dom_list[2]=last_30_days_date
|
||||||
|
|
||||||
|
# elif right_value == 'last_90_days':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_90_days_date=current_date+ timedelta(days=-90)
|
||||||
|
# dom_list[2]=last_90_days_date
|
||||||
|
|
||||||
|
# elif right_value == 'last_365_days':
|
||||||
|
# dom_list[1]='>='
|
||||||
|
# last_365_days_date=current_date+ timedelta(days=-365)
|
||||||
|
# dom_list[2]=last_365_days_date
|
||||||
|
|
||||||
|
# elif right_value == "next_day":
|
||||||
|
# next_day_date=current_date+ timedelta(days=+1)
|
||||||
|
# dom_list[1]='>'
|
||||||
|
# last_365_days_date=current_date+ timedelta(days=-365)
|
||||||
|
# dom_list[2]=last_365_days_date
|
||||||
|
# pass
|
||||||
|
# elif right_value == "next_week":
|
||||||
|
# pass
|
||||||
|
# elif right_value == "next_month":
|
||||||
|
# pass
|
||||||
|
# elif right_value == "next_year":
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# dom_tuple = dom_list
|
||||||
|
# prepared_domain.append(tuple(dom_list))
|
||||||
|
# if today_end_time:
|
||||||
|
# prepared_domain.append(tuple(today_end_time))
|
||||||
|
# if this_qua_end_dom_list:
|
||||||
|
# prepared_domain.append(tuple(this_qua_end_dom_list))
|
||||||
|
# if last_qua_end_dom_list:
|
||||||
|
# prepared_domain.append(tuple(last_qua_end_dom_list))
|
||||||
|
# if last_year_end_dom_list:
|
||||||
|
# prepared_domain.append(tuple(last_year_end_dom_list))
|
||||||
|
# if this_year_end_dom_list:
|
||||||
|
# prepared_domain.append(tuple(this_year_end_dom_list))
|
||||||
|
# if last_week_end_date_dom_list:
|
||||||
|
# prepared_domain.append(tuple(last_week_end_date_dom_list))
|
||||||
|
# if last_month_end_date_dom_list:
|
||||||
|
# prepared_domain.append(tuple(last_month_end_date_dom_list))
|
||||||
|
|
||||||
|
# else:
|
||||||
|
# prepared_domain.append(dom_tuple)
|
||||||
|
# return prepared_domain
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(models.AbstractModel):
|
||||||
|
_inherit = 'base'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_widget_name(self, domain=None, offset=0, limit=None, order=None):
|
||||||
|
"""Get records for the advanced domain widget"""
|
||||||
|
return self.sudo().search_read(domain or [], ['id', 'display_name'], offset=offset, limit=limit, order=order)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_widget_count(self, domain):
|
||||||
|
"""Get record count for the advanced domain widget"""
|
||||||
|
return self.sudo().search_count(domain or [])
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 701 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 389 B |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
|
@ -0,0 +1,545 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US" data-website-id="1" data-oe-company-name="Odoo S.A.">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<link type="text/css" rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" />
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.js"
|
||||||
|
integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Laila:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
|
||||||
|
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link type="text/css" rel="stylesheet" href="/assets/assets.css" />
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="module-description">
|
||||||
|
<!-- Keep From Here -->
|
||||||
|
<img class="img img-responsive center-block" style="border-top-left-radius:10px; border-top-right-radius:10px" src="https://www.terabits.xyz/index/img/advanced_web_domain_widget/14.0/img.png">
|
||||||
|
<div class="container">
|
||||||
|
<div class="oe_styling_v8">
|
||||||
|
|
||||||
|
<!-- elearning post starts -->
|
||||||
|
|
||||||
|
<section class="blog_post_01">
|
||||||
|
<div class="position-relative w-100" style="display: inline-grid;">
|
||||||
|
<img src="img/screens/domain_bg_img_01.png"
|
||||||
|
class="img-responsive img img-fluid w-100 h-100 img img-fluid position-absolute left_0" />
|
||||||
|
|
||||||
|
<div class="position-relative row justify-content-center align-items-center"
|
||||||
|
style="padding: 20px 10px 40px 10px;">
|
||||||
|
<div class="col-12 my-3 px-5">
|
||||||
|
<div class="header-img">
|
||||||
|
<img src="img/screens/domain_header_img.png" class="img img-fluid w-100"
|
||||||
|
style="width: 100% !important;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="blg-01-content text-center my-5">
|
||||||
|
<h1
|
||||||
|
style="font-family: 'Inter', sans-serif; font-size: 45px; font-weight: 700;line-height: 51px;color: #151765;">
|
||||||
|
Advanced web domian widget
|
||||||
|
</h1>
|
||||||
|
<h3
|
||||||
|
style="font-family: 'Inter', sans-serif; font-size: 22px; font-weight: 600;line-height: 31px; color: #1947AE;margin-top: 10px;">
|
||||||
|
"Now use the feature of select any models record in domain while using any relational field."
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
style="font-family: 'Inter', sans-serif; font-size: 18px; font-weight: 500;line-height: 27px; color: #000000; margin-top: 20px;">
|
||||||
|
Odoo base domain widget allows you to only match value or id while user wants to create <br/>
|
||||||
|
domain using any relational fields. So, user confused when model has multiple record's id <br/>
|
||||||
|
and he/she does't remembered. So, we have simplified that by showing models record to <br/>
|
||||||
|
the user. so, he/she can select by finding record and select it. our module will autometic <br/>
|
||||||
|
adds ids of selected records in domain. To select related model's record and create <br/>
|
||||||
|
domain, we allowed additional two domain operators ('in', 'not in').
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- elearning post end -->
|
||||||
|
|
||||||
|
<!-- features highlight -->
|
||||||
|
|
||||||
|
<section class="domain_features_02">
|
||||||
|
<div class="position-relative w-100" style="display: inline-grid;">
|
||||||
|
<img src="img/screens/domain_bg_img_02.png" class="img-responsive w-100 h-100 img img-fluid position-absolute left_0"/>
|
||||||
|
|
||||||
|
<div class="position-relative row justify-content-center align-items-center" style="padding: 20px 10px 40px 10px;">
|
||||||
|
<div class="col-12 my-3 px-5">
|
||||||
|
<div class="title text-center">
|
||||||
|
<h2 style="font-family: 'Inter', sans-serif; font-size: 45px; font-weight: 700;line-height: 61px;color: #151765;">
|
||||||
|
Features
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-around align-items-center mt-4">
|
||||||
|
<div style="background-color: #F1F7FF; padding: 20px 40px 20px 25px; border-radius: 8px; width: 45% !important; height: 100% !important;" class="shadow-sm">
|
||||||
|
<h6 style="font-family: 'Inter', sans-serif; font-size: 25px; font-weight: 600;line-height: 36px;color: #151765;">
|
||||||
|
Select any models records
|
||||||
|
</h6>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p style="font-family: 'Inter', sans-serif; font-size: 17px; font-weight: 500;line-height: 24px;color: #151765;">
|
||||||
|
Easy to create domain of relational fields by <br/>
|
||||||
|
selecting any models record in domain. We <br/>
|
||||||
|
provide additional operators ('in' and 'not in') <br/>
|
||||||
|
to create relational fields domain.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background-color: #F1F7FF; padding: 32px 40px 32px 25px; border-radius: 8px; width: 45% !important; height: 100% !important;" class="shadow-sm">
|
||||||
|
<h6 style="font-family: 'Inter', sans-serif; font-size: 25px; font-weight: 600;line-height: 36px;color: #151765;">
|
||||||
|
Autometic id add in domain
|
||||||
|
</h6>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p style="font-family: 'Inter', sans-serif; font-size: 17px; font-weight: 500;line-height: 24px;color: #151765;">
|
||||||
|
When user select models records from popup, <br/>
|
||||||
|
there will generate tags of record's names and <br/>
|
||||||
|
add records id in domain.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- features highlight -->
|
||||||
|
|
||||||
|
<!-- tabs -->
|
||||||
|
<section class="info-tabs mt-5">
|
||||||
|
<div>
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="justify-content-center d-flex">
|
||||||
|
<!-- Nav pills -->
|
||||||
|
<ul class="nav nav-tabs justify-content-center" style="border-radius: 6px 6px; background-color: #ededed; padding:9px 14px 8px 14px;" role="tablist" data-tabs="tabs">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="show "
|
||||||
|
id="v-guide-tab" data-bs-toggle="pill" aria-controls="v-guide" href="#pills-guide" aria-expanded="true" style="padding: 11px 12px 8px 12px !important;">
|
||||||
|
<span class="m-0" style="font-weight: 600; font-family: 'Inter', sans-serif; color: #1c29da;font-size: 19px; line-height: 23px;">USERGUIDE</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item mx-1">
|
||||||
|
<a class="show active"
|
||||||
|
id="v-demo-tab" data-bs-toggle="pill" aria-controls="v-ss" href="#pills-screenshot" aria-expanded="true" style="padding: 11px 12px 8px 12px !important;">
|
||||||
|
<span class="m-0" style="font-weight: 600; font-family: 'Inter', sans-serif; color: #1c29da;font-size: 19px; line-height: 23px">DEMO</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item mx-1">
|
||||||
|
<a class="show"
|
||||||
|
id="v-faqs-tab" data-bs-toggle="pill" aria-controls="v-faqs" href="#pills-faqs" aria-expanded="true" style="padding: 11px 12px 8px 12px !important;">
|
||||||
|
<span class="m-0" style="font-weight: 600; font-family: 'Inter', sans-serif; color: #1c29da;font-size: 19px; line-height: 23px">FAQS</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item ">
|
||||||
|
<a class="show"
|
||||||
|
id="v-release-tab" data-bs-toggle="pill" aria-controls="v-release" href="#pills-release" aria-expanded="true" style="padding: 11px 12px 8px 12px !important;">
|
||||||
|
<span class="m-0" style="font-weight: 600; font-family: 'Inter', sans-serif; color: #1c29da;font-size: 19px; line-height: 23px">RELEASES</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content" style="border-radius: 10px;"
|
||||||
|
id="pills-tabContent">
|
||||||
|
<!-- user-guide -->
|
||||||
|
<div class="tab-pane fade py-3 shadow" id="pills-guide" role="tabpanel"
|
||||||
|
aria-labelledby="v-guide-tab" aria-labelledby="pills-guide-tab">
|
||||||
|
|
||||||
|
<div class="screenshot-description" style="margin: 1% 3%;">
|
||||||
|
<div style="border-radius: 10px;background-color: #f9f9f9;text-align:center;">
|
||||||
|
<h4 style='font-weight: 600;padding: 20px;'>You just need to change in xml
|
||||||
|
files to use our advanced domain feature.
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="screenshot-description" style="margin: 1% 3%;">
|
||||||
|
<div style="border-radius: 10px;background-color: #f9f9f9;">
|
||||||
|
<li style='font-weight: 600;padding: 20px;'>
|
||||||
|
<span>Replace name of odoo's 'domain' widget to 'terabits_domain'
|
||||||
|
widget.
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="screenshot" style="text-align: center;">
|
||||||
|
<img class="img img-fluid oe_screenshot" style="width: 90%;margin: 2%;"
|
||||||
|
src="img/screens/ss4.png" alt="Odoo's domain widget" />
|
||||||
|
</div>
|
||||||
|
<div class="screenshot" style="text-align: center;">
|
||||||
|
<img class="img img-fluid oe_screenshot" style="width: 90%;margin: 2%;"
|
||||||
|
src="img/screens/ss5.png" alt="Terabits's domain widget" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- screenshots tab -->
|
||||||
|
<div class="tab-pane fade text-center active show" id="pills-screenshot" role="tabpanel"
|
||||||
|
aria-labelledby="v-ss-tab" aria-labelledby="pills-screenshot-tab">
|
||||||
|
|
||||||
|
<!-- 01 -->
|
||||||
|
|
||||||
|
<section class="blog_post_01">
|
||||||
|
<div class="position-relative w-100" style="display: inline-grid;">
|
||||||
|
<img src="img/screens/domain_bg_img_03.png"
|
||||||
|
class="img-responsive w-100 h-100 img img-fluid position-absolute left_0" />
|
||||||
|
|
||||||
|
<div class="position-relative row justify-content-center align-items-center"
|
||||||
|
style="padding: 20px 20px 40px 20px;">
|
||||||
|
<div class="col-12 my-3 px-5">
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="content-note d-flex">
|
||||||
|
<div class="icon-img">
|
||||||
|
<img src="img/screens/icon_img.png"
|
||||||
|
class="img img-fluid" />
|
||||||
|
</div>
|
||||||
|
<div class="content"
|
||||||
|
style="margin-left: 15px !important;">
|
||||||
|
<p
|
||||||
|
style="font-family: 'Inter', sans-serif; font-size: 20px; font-weight: 500;line-height: 26px;color: #151765;">
|
||||||
|
Here is odoo's 'domain' widget for domain creation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-img text-center mt-3 mx-5">
|
||||||
|
<img src="img/screens/ss1.png"
|
||||||
|
class="img img-fluid img-responsive" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<div class="content-note d-flex ">
|
||||||
|
<div class="icon-img">
|
||||||
|
<img src="img/screens/icon_img.png"
|
||||||
|
class="img img-fluid" />
|
||||||
|
</div>
|
||||||
|
<div class="content"
|
||||||
|
style="margin-left: 15px !important;">
|
||||||
|
<p
|
||||||
|
style="font-family: 'Inter', sans-serif; font-size: 20px; font-weight: 500;line-height: 26px;color: #151765;">
|
||||||
|
Here is customized 'terabits_domain' widget for domain creation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-img text-center mt-3 mx-5">
|
||||||
|
<img src="img/screens/ss2.png"
|
||||||
|
class="img img-fluid img-responsive" />
|
||||||
|
</div>
|
||||||
|
<div class="content-img text-center mt-4 mx-5">
|
||||||
|
<img src="img/screens/ss3.png"
|
||||||
|
class="img img-fluid img-responsive" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<div class="content-note d-flex ">
|
||||||
|
<div class="icon-img">
|
||||||
|
<img src="img/screens/icon_img.png"
|
||||||
|
class="img img-fluid" />
|
||||||
|
</div>
|
||||||
|
<div class="content"
|
||||||
|
style="margin-left: 15px !important;">
|
||||||
|
<p
|
||||||
|
style="font-family: 'Inter', sans-serif; font-size: 20px; font-weight: 500;line-height: 26px;color: #151765;">
|
||||||
|
Here is customized 'date and filter' widget for domain creation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-img text-center mt-3 mx-5">
|
||||||
|
<img src="img/screens/date_filter_ss4.png"
|
||||||
|
class="img img-fluid img-responsive" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<div class="content-note d-flex ">
|
||||||
|
<div class="icon-img">
|
||||||
|
<img src="img/screens/icon_img.png"
|
||||||
|
class="img img-fluid" />
|
||||||
|
</div>
|
||||||
|
<div class="content"
|
||||||
|
style="margin-left: 15px !important;">
|
||||||
|
<p
|
||||||
|
style="font-family: 'Inter', sans-serif; font-size: 20px; font-weight: 500;line-height: 26px;color: #151765;">
|
||||||
|
Here is how you can select environment company and user.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-img text-center mt-3 mx-5">
|
||||||
|
<img src="img/screens/date_filter_ss5.png"
|
||||||
|
class="img img-fluid img-responsive" />
|
||||||
|
</div>
|
||||||
|
<div class="content-img text-center mt-4 mx-5">
|
||||||
|
<img src="img/screens/date_filter_ss6.png"
|
||||||
|
class="img img-fluid img-responsive" />
|
||||||
|
</div>
|
||||||
|
<div class="content-img text-center mt-4 mx-5">
|
||||||
|
<img src="img/screens/date_filter_ss7.png"
|
||||||
|
class="img img-fluid img-responsive" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- faq tab -->
|
||||||
|
<div class="tab-pane fade shadow py-3" id="pills-faqs" role="tabpanel" aria-labelledby="v-faqs-tab"
|
||||||
|
aria-labelledby="pills-faqs-tab">
|
||||||
|
<div class="s_faq mt32 mb32"
|
||||||
|
style="background-color: transparent !important;padding: 40px 50px;">
|
||||||
|
<div class="panel-group" id="accordion" role="tablist"
|
||||||
|
aria-multiselectable="true">
|
||||||
|
|
||||||
|
<div class="panel-group"
|
||||||
|
style="border: 1px solid #ddd;border-radius: 10px;margin-bottom: 20px;">
|
||||||
|
<div class="panel panel-default"
|
||||||
|
style="box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);padding: 15px 15px;border-radius: 10px;">
|
||||||
|
<div class="panel-heading mt0"
|
||||||
|
style="border:1px solid transparent !important">
|
||||||
|
<h4 class="panel-title" style="margin: 0;">
|
||||||
|
<a class="collapsed"
|
||||||
|
data-bs-toggle="collapse" href="#collapse1"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span class="panelcontent" style="font-family: 'Inter', sans-serif;font-size: 20px;font-weight: 500;line-height: 25px;color: #000;">
|
||||||
|
Why should I use this app?
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse1"
|
||||||
|
style="background-color: rgb(240, 244, 247); padding: 15px;"
|
||||||
|
class="panel-collapse collapse">
|
||||||
|
<div>
|
||||||
|
<p class="mb0"
|
||||||
|
style="font-family: 'Inter', sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;
|
||||||
|
color: #000; padding-left:2%; margin-left:2%">
|
||||||
|
This module provides domains with an additional feature
|
||||||
|
to select any models record while using any relational
|
||||||
|
field and create a domain after selecting it. for that,
|
||||||
|
we provide two additional operators ('in' and 'not in')
|
||||||
|
that allow the user to select
|
||||||
|
a record of any model. after select any record, its id
|
||||||
|
automatic adds in domain.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-group"
|
||||||
|
style="border: 1px solid #ddd;border-radius: 10px;margin-bottom: 20px;">
|
||||||
|
<div class="panel panel-default"
|
||||||
|
style="box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);padding: 15px 15px;border-radius: 10px;">
|
||||||
|
<div class="panel-heading mt0"
|
||||||
|
style="border:1px solid transparent !important">
|
||||||
|
<h4 class="panel-title" style="margin: 0;">
|
||||||
|
<a class="collapsed"
|
||||||
|
data-bs-toggle="collapse" href="#collapse2"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span class="panelcontent" style="font-family: 'Inter', sans-serif;font-size: 20px;font-weight: 500;line-height: 25px;color: #000;">
|
||||||
|
What is user's main benifit ?
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse2"
|
||||||
|
style="background-color: rgb(240, 244, 247); padding: 15px;"
|
||||||
|
class="panel-collapse collapse">
|
||||||
|
<div>
|
||||||
|
<p class="mb0"
|
||||||
|
style="font-family: 'Inter', sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;
|
||||||
|
color: #000; padding-left:2%; margin-left:2%">
|
||||||
|
User's main benifit is that, he/she does not have to
|
||||||
|
remember models record id while he/she have to create
|
||||||
|
domain based on relational fields, because we direct
|
||||||
|
show all models record so user only have to select its
|
||||||
|
record.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-group"
|
||||||
|
style="border: 1px solid #ddd;border-radius: 10px;margin-bottom: 20px;">
|
||||||
|
<div class="panel panel-default"
|
||||||
|
style="box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);padding: 15px 15px;border-radius: 10px;">
|
||||||
|
<div class="panel-heading mt0"
|
||||||
|
style="border:1px solid transparent !important">
|
||||||
|
<h4 class="panel-title" style="margin: 0;">
|
||||||
|
<a class="collapsed"
|
||||||
|
data-bs-toggle="collapse" href="#collapse3"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span class="panelcontent" style="font-family: 'Inter', sans-serif;font-size: 20px;font-weight: 500;line-height: 25px;color: #000;">
|
||||||
|
Need some customization in this app, whom can I
|
||||||
|
contact?
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse3"
|
||||||
|
style="background-color: rgb(240, 244, 247); padding: 15px;"
|
||||||
|
class="panel-collapse collapse">
|
||||||
|
<div>
|
||||||
|
<p class="mb0"
|
||||||
|
style="font-family: 'Inter', sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;
|
||||||
|
color: #000; padding-left:2%; margin-left:2%">
|
||||||
|
Please drop an email at info@terabits.xyz or raise a
|
||||||
|
ticket through the Odoo store itself.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-group"
|
||||||
|
style="border: 1px solid #ddd;border-radius: 10px;margin-bottom: 20px;">
|
||||||
|
<div class="panel panel-default"
|
||||||
|
style="box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);padding: 15px 15px;border-radius: 10px;">
|
||||||
|
<div class="panel-heading mt0"
|
||||||
|
style="border:1px solid transparent !important">
|
||||||
|
<h4 class="panel-title" style="margin: 0;">
|
||||||
|
<a class="collapsed"
|
||||||
|
data-bs-toggle="collapse" href="#collapse4"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span class="panelcontent" style="font-family: 'Inter', sans-serif;font-size: 20px;font-weight: 500;line-height: 25px;color: #000;">
|
||||||
|
Do you provide any free support?
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse4"
|
||||||
|
style="background-color: rgb(240, 244, 247); padding: 15px;"
|
||||||
|
class="panel-collapse collapse">
|
||||||
|
<div>
|
||||||
|
<p class="mb0"
|
||||||
|
style="font-family: 'Inter', sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;
|
||||||
|
color: #000; padding-left:2%; margin-left:2%">
|
||||||
|
Yes, I do provide free support for 90 days for any
|
||||||
|
queries or any bug/issue fixing.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-group"
|
||||||
|
style="border: 1px solid #ddd;border-radius: 10px;margin-bottom: 20px;">
|
||||||
|
<div class="panel panel-default"
|
||||||
|
style="box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);padding: 15px 15px;border-radius: 10px;">
|
||||||
|
<div class="panel-heading mt0"
|
||||||
|
style="border:1px solid transparent !important">
|
||||||
|
<h4 class="panel-title" style="margin: 0;">
|
||||||
|
<a class="collapsed"
|
||||||
|
data-bs-toggle="collapse" href="#collapse5"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span class="panelcontent" style="font-family: 'Inter', sans-serif;font-size: 20px;font-weight: 500;line-height: 25px;color: #000;">
|
||||||
|
What is Support Policy of this Module?
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="collapse5"
|
||||||
|
style="background-color: rgb(240, 244, 247); padding: 15px;"
|
||||||
|
class="panel-collapse collapse">
|
||||||
|
<div>
|
||||||
|
<p class="mb0"
|
||||||
|
style="font-family: 'Inter', sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;
|
||||||
|
color: #000; padding-left:2%; margin-left:2%">
|
||||||
|
In case of if any bug raised in the listed features of
|
||||||
|
this module, I am committed to providing support free of
|
||||||
|
cost. You will need to provide me server ssh access or
|
||||||
|
database access in order to solve the issue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- release tab -->
|
||||||
|
<div class="tab-pane px-0 px-sm-5 shadow fade" id="pills-release" aria-labelledby="v-release-tab" style="padding-top: 0.25rem !important;" role="tabpanel" aria-labelledby="pills-release-tab">
|
||||||
|
|
||||||
|
<div class="row my-2 p-4 position-relative">
|
||||||
|
<div class="col-12 px-0 bg-white">
|
||||||
|
<h2 style="font-family: 'Inter', sans-serif;font-size: 35px;color: #000;font-weight: 600;">Changelog(s)</h2>
|
||||||
|
<hr class="mb-4 mt-0">
|
||||||
|
<!-- v14.0.1.0.2 -->
|
||||||
|
<h2 class="h4 pb-2">
|
||||||
|
<span style=" font-family: 'Inter', sans-serif; color:#00B5DB;font-size: 22px;font-weight: 600;" class="pe-2">v14.0.1.0.2</span>
|
||||||
|
<span>-</span>
|
||||||
|
<span class="ps-2" style="font-family: 'Inter', sans-serif;font-size: 23px;font-weight: 500;color: #000;">December 10, 2022</span>
|
||||||
|
</h2>
|
||||||
|
<p style="font-family: 'Inter', sans-serif;color: #000;font-size: 18px; font-weight: 500; line-height: 28px;">
|
||||||
|
Minor bug fix.
|
||||||
|
</p>
|
||||||
|
<!-- v14.0.0.0.0 -->
|
||||||
|
<h2 class="h4 pb-2">
|
||||||
|
<span style=" font-family: 'Inter', sans-serif; color:#00B5DB;font-size: 22px;font-weight: 600;" class="pe-2">v14.0.0.0.0</span>
|
||||||
|
<span>-</span>
|
||||||
|
<span class="ps-2" style="font-family: 'Inter', sans-serif;font-size: 23px;font-weight: 500;color: #000;">November 23, 2022</span>
|
||||||
|
</h2>
|
||||||
|
<p style="font-family: 'Inter', sans-serif;color: #000;font-size: 18px; font-weight: 500; line-height: 28px;">
|
||||||
|
Initial release for v14
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- tabs end -->
|
||||||
|
|
||||||
|
<!-- Help START -->
|
||||||
|
<div class="row pb16 pt-5 mb-5">
|
||||||
|
<div class="alert alert-warning text-center w-100"
|
||||||
|
style="padding:21px 51px; background-color:#ffffff; border:1px solid #dee2e6; color:#414d5c; margin:auto; display:block; border-radius:1px; min-width:90%; border-top:3px solid #151765">
|
||||||
|
<div>
|
||||||
|
<div style="background-color:rgb(255 164 0 / 12%); color:#151765"
|
||||||
|
class="badge border-0 rounded-circle p-3">
|
||||||
|
<i class="fa fa-question-circle fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 style="color:#3c4858" class="mt-2 mb-4">
|
||||||
|
Need a help for this module?
|
||||||
|
</h2>
|
||||||
|
<h4 style="color:#3c4858; font-weight:400" class="mt-2 mb-4">
|
||||||
|
Contact me
|
||||||
|
<b
|
||||||
|
style="color:#151765; background-color:#e0f0ff; padding:3px 10px; border-radius:3px">info@terabits.xyz</b>
|
||||||
|
for your queries
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Help END -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||||
|
import { Dialog } from "@web/core/dialog/dialog";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced Domain Selector Dialog Component
|
||||||
|
*/
|
||||||
|
export class AdvancedDomainSelectorDialog extends Component {
|
||||||
|
static template = "advanced_web_domain_widget.DomainSelectorDialog";
|
||||||
|
static components = { Dialog };
|
||||||
|
static props = {
|
||||||
|
resModel: String,
|
||||||
|
domain: String,
|
||||||
|
onSave: Function,
|
||||||
|
close: Function,
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.orm = useService("orm");
|
||||||
|
this.state = useState({
|
||||||
|
domain: this.props.domain || "[]",
|
||||||
|
selectedRecords: [],
|
||||||
|
searchValue: "",
|
||||||
|
availableRecords: [],
|
||||||
|
isLoading: false
|
||||||
|
});
|
||||||
|
|
||||||
|
onWillStart(async () => {
|
||||||
|
await this.loadAvailableRecords();
|
||||||
|
this.parseExistingDomain();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parseExistingDomain() {
|
||||||
|
try {
|
||||||
|
if (this.props.domain && this.props.domain !== "[]") {
|
||||||
|
// Parse domain to extract selected IDs
|
||||||
|
const domainStr = this.props.domain;
|
||||||
|
const match = domainStr.match(/\[.*'id'.*'in'.*\[([^\]]+)\]/);
|
||||||
|
if (match) {
|
||||||
|
const idsStr = match[1];
|
||||||
|
const ids = idsStr.split(',').map(id => parseInt(id.trim()));
|
||||||
|
this.state.selectedRecords = ids.filter(id => !isNaN(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing domain:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAvailableRecords() {
|
||||||
|
this.state.isLoading = true;
|
||||||
|
try {
|
||||||
|
const records = await this.orm.searchRead(
|
||||||
|
this.props.resModel,
|
||||||
|
[],
|
||||||
|
["display_name"],
|
||||||
|
{ limit: 100 }
|
||||||
|
);
|
||||||
|
this.state.availableRecords = records;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading records:", error);
|
||||||
|
this.state.availableRecords = [];
|
||||||
|
} finally {
|
||||||
|
this.state.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchInput(ev) {
|
||||||
|
this.state.searchValue = ev.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get filteredRecords() {
|
||||||
|
const search = this.state.searchValue.toLowerCase();
|
||||||
|
if (!search) return this.state.availableRecords;
|
||||||
|
|
||||||
|
return this.state.availableRecords.filter(record =>
|
||||||
|
record.display_name.toLowerCase().includes(search)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRecord(recordId) {
|
||||||
|
const index = this.state.selectedRecords.indexOf(recordId);
|
||||||
|
if (index === -1) {
|
||||||
|
this.state.selectedRecords.push(recordId);
|
||||||
|
} else {
|
||||||
|
this.state.selectedRecords.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.updateDomain();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDomain() {
|
||||||
|
if (this.state.selectedRecords.length > 0) {
|
||||||
|
this.state.domain = `[('id', 'in', [${this.state.selectedRecords.join(',')}])]`;
|
||||||
|
} else {
|
||||||
|
this.state.domain = "[]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave() {
|
||||||
|
this.props.onSave(this.state.domain);
|
||||||
|
this.props.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.props.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced Domain Field Widget
|
||||||
|
*/
|
||||||
|
export class AdvancedDomainField extends Component {
|
||||||
|
static template = "advanced_web_domain_widget.DomainField";
|
||||||
|
static props = {
|
||||||
|
...standardFieldProps,
|
||||||
|
resModel: { type: String, optional: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.dialog = useService("dialog");
|
||||||
|
this.orm = useService("orm");
|
||||||
|
this.state = useState({
|
||||||
|
domainString: this.props.value || "[]"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayValue() {
|
||||||
|
try {
|
||||||
|
if (!this.state.domainString || this.state.domainString === "[]") {
|
||||||
|
return "No domain set";
|
||||||
|
}
|
||||||
|
return this.state.domainString;
|
||||||
|
} catch {
|
||||||
|
return this.state.domainString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openAdvancedSelector() {
|
||||||
|
const resModel = this.props.resModel ||
|
||||||
|
(this.props.record && this.props.record.resModel) ||
|
||||||
|
'res.partner'; // fallback
|
||||||
|
|
||||||
|
this.dialog.add(AdvancedDomainSelectorDialog, {
|
||||||
|
resModel: resModel,
|
||||||
|
domain: this.state.domainString,
|
||||||
|
onSave: (domain) => {
|
||||||
|
this.state.domainString = domain;
|
||||||
|
this.props.update(domain);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDirectEdit(ev) {
|
||||||
|
this.state.domainString = ev.target.value;
|
||||||
|
this.props.update(ev.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the field widget
|
||||||
|
registry.category("fields").add("advanced_domain", AdvancedDomainField);
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
.o_field_advanced_domain {
|
||||||
|
.input-group {
|
||||||
|
.btn {
|
||||||
|
border-color: var(--o-input-border-color);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--o-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_advanced_domain_selector {
|
||||||
|
.list-group-item {
|
||||||
|
cursor: pointer;
|
||||||
|
&.active {
|
||||||
|
background-color: var(--o-primary);
|
||||||
|
border-color: var(--o-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
&:hover:not(.active) {
|
||||||
|
background-color: var(--o-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates>
|
||||||
|
<t t-name="advanced_web_domain_widget.DomainField">
|
||||||
|
<div class="o_field_advanced_domain">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
t-att-value="displayValue"
|
||||||
|
t-att-readonly="props.readonly"
|
||||||
|
t-on-change="onDirectEdit"
|
||||||
|
placeholder="Enter domain..."/>
|
||||||
|
<button t-if="!props.readonly"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
t-on-click="openAdvancedSelector">
|
||||||
|
<i class="fa fa-search"/> Advanced
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="advanced_web_domain_widget.DomainSelectorDialog">
|
||||||
|
<Dialog title="'Advanced Domain Selector'" size="'lg'">
|
||||||
|
<div class="o_advanced_domain_selector">
|
||||||
|
<t t-if="state.isLoading">
|
||||||
|
<div class="text-center p-4">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">Loading records...</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Available Records</h5>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control mb-2"
|
||||||
|
placeholder="Search..."
|
||||||
|
t-model="state.searchValue"
|
||||||
|
t-on-input="onSearchInput"/>
|
||||||
|
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
||||||
|
<t t-if="filteredRecords.length == 0">
|
||||||
|
<div class="text-muted text-center p-3">
|
||||||
|
<t t-if="state.searchValue">
|
||||||
|
No records found matching your search.
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
No records available.
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-foreach="filteredRecords" t-as="record" t-key="record.id">
|
||||||
|
<a href="#"
|
||||||
|
class="list-group-item list-group-item-action"
|
||||||
|
t-att-class="{ active: state.selectedRecords.includes(record.id) }"
|
||||||
|
t-on-click.prevent="() => toggleRecord(record.id)">
|
||||||
|
<i t-if="state.selectedRecords.includes(record.id)" class="fa fa-check-square-o me-2"/>
|
||||||
|
<i t-else="" class="fa fa-square-o me-2"/>
|
||||||
|
<t t-esc="record.display_name"/>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Domain Preview</h5>
|
||||||
|
<pre class="bg-light p-3 rounded border">
|
||||||
|
<t t-esc="state.domain"/>
|
||||||
|
</pre>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6 class="text-primary">
|
||||||
|
Selected Records:
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
<t t-esc="state.selectedRecords.length"/>
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
<t t-if="state.selectedRecords.length > 0">
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">
|
||||||
|
Selected IDs: <t t-esc="state.selectedRecords.join(', ')"/>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<t t-set-slot="footer">
|
||||||
|
<button class="btn btn-primary" t-on-click="onSave">Save Domain</button>
|
||||||
|
<button class="btn btn-secondary" t-on-click="onCancel">Cancel</button>
|
||||||
|
</t>
|
||||||
|
</Dialog>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
Most of the files are
|
||||||
|
|
||||||
|
Copyright (c) 2019 - present Droggol.
|
||||||
|
|
||||||
|
Some javascript files might be from from third
|
||||||
|
parties libraries. In that case the original
|
||||||
|
copyright of the contributions can be traced
|
||||||
|
through the history of the source version
|
||||||
|
control system.
|
||||||
|
|
||||||
|
When that is not the case, the files contain a prominent
|
||||||
|
notice stating the original copyright and applicable
|
||||||
|
license, or come with their own dedicated COPYRIGHT
|
||||||
|
and/or LICENSE file.
|
||||||
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
Odoo Proprietary License v1.0
|
||||||
|
|
||||||
|
This software and associated files (the "Software") may only be used (executed,
|
||||||
|
modified, executed after modifications) if you have purchased a valid license
|
||||||
|
from the authors, typically via Odoo Apps, or if you have received a written
|
||||||
|
agreement from the authors of the Software (see the COPYRIGHT file).
|
||||||
|
|
||||||
|
You may develop Odoo modules that use the Software as a library (typically
|
||||||
|
by depending on it, importing it and using its resources), but without copying
|
||||||
|
any source code or material from the Software. You may distribute those
|
||||||
|
modules under the license of your choice, provided that this license is
|
||||||
|
compatible with the terms of the Odoo Proprietary License (For example:
|
||||||
|
LGPL, MIT, or proprietary licenses similar to this one).
|
||||||
|
|
||||||
|
It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||||
|
or modified copies of the Software.
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice must be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Droggol. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Droggol. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Form Double Click Edit',
|
||||||
|
'summary': 'Double click on form view to Edit or Save',
|
||||||
|
'description': """
|
||||||
|
This module provides the feature that allows to edit/save formview record on double click.
|
||||||
|
""",
|
||||||
|
|
||||||
|
'version': '18.0.0.1',
|
||||||
|
'license': 'OPL-1',
|
||||||
|
'category': 'Odex25-base',
|
||||||
|
'author': 'Droggol',
|
||||||
|
'company': 'Droggol',
|
||||||
|
'maintainer': 'Droggol',
|
||||||
|
'website': 'https://www.droggol.com/',
|
||||||
|
|
||||||
|
'depends': ['web'],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'droggol_dblclick_edit/static/src/js/form_controller.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'images': ['static/description/images/banner.png'],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'application': False,
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 40' width='80' height='40'><path fill='#ffffff' fill-opacity='0.15' d='M0 40a19.96 19.96 0 0 1 5.9-14.11 20.17 20.17 0 0 1 19.44-5.2A20 20 0 0 1 20.2 40H0zM65.32.75A20.02 20.02 0 0 1 40.8 25.26 20.02 20.02 0 0 1 65.32.76zM.07 0h20.1l-.08.07A20.02 20.02 0 0 1 .75 5.25 20.08 20.08 0 0 1 .07 0zm1.94 40h2.53l4.26-4.24v-9.78A17.96 17.96 0 0 0 2 40zm5.38 0h9.8a17.98 17.98 0 0 0 6.67-16.42L7.4 40zm3.43-15.42v9.17l11.62-11.59c-3.97-.5-8.08.3-11.62 2.42zm32.86-.78A18 18 0 0 0 63.85 3.63L43.68 23.8zm7.2-19.17v9.15L62.43 2.22c-3.96-.5-8.05.3-11.57 2.4zm-3.49 2.72c-4.1 4.1-5.81 9.69-5.13 15.03l6.61-6.6V6.02c-.51.41-1 .85-1.48 1.33zM17.18 0H7.42L3.64 3.78A18 18 0 0 0 17.18 0zM2.08 0c-.01.8.04 1.58.14 2.37L4.59 0H2.07z'></path></svg>
|
||||||
|
After Width: | Height: | Size: 785 B |
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg6"
|
||||||
|
sodipodi:docname="lnr-cloud-upload.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata12">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs10" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview8"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M16.006 16h-3.506c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h3.506c1.651 0 2.994-1.343 2.994-2.994s-1.343-2.994-2.994-2.994c-0.352 0-0.696 0.060-1.023 0.179-0.218 0.079-0.462-0.002-0.589-0.196s-0.104-0.45 0.056-0.618c0.355-0.373 0.55-0.862 0.55-1.377 0-1.103-0.897-2-2-2-0.642 0-1.229 0.297-1.61 0.814-0.229 0.31-0.362 0.677-0.386 1.061-0.013 0.212-0.159 0.393-0.364 0.451s-0.423-0.021-0.545-0.195l-0.005-0.007c-0.107-0.152-0.226-0.302-0.351-0.442-0.949-1.068-2.312-1.681-3.74-1.681-2.757 0-5 2.243-5 5s2.243 5 5 5h2.5c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5h-2.5c-3.308 0-6-2.692-6-6s2.692-6 6-6c1.603 0 3.137 0.643 4.261 1.775 0.087-0.195 0.196-0.381 0.324-0.555 0.564-0.764 1.467-1.22 2.415-1.22 1.654 0 3 1.346 3 3 0 0.351-0.061 0.694-0.176 1.017 0.061-0.003 0.122-0.004 0.183-0.004 2.202 0 3.994 1.792 3.994 3.994s-1.792 3.994-3.994 3.994z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ab47bc;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M12.854 12.146l-2-2c-0.195-0.195-0.512-0.195-0.707 0l-2 2c-0.195 0.195-0.195 0.512 0 0.707s0.512 0.195 0.707 0l1.146-1.146v3.793c0 0.276 0.224 0.5 0.5 0.5s0.5-0.224 0.5-0.5v-3.793l1.146 1.146c0.098 0.098 0.226 0.146 0.354 0.146s0.256-0.049 0.354-0.146c0.195-0.195 0.195-0.512 0-0.707z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#ab47bc;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="lnr-code.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata14">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs12" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg8" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M5 15c-0.128 0-0.256-0.049-0.354-0.146l-4-4c-0.195-0.195-0.195-0.512 0-0.707l4-4c0.195-0.195 0.512-0.195 0.707 0s0.195 0.512 0 0.707l-3.646 3.646 3.646 3.646c0.195 0.195 0.195 0.512 0 0.707-0.098 0.098-0.226 0.146-0.354 0.146z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#0080ff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M15 15c-0.128 0-0.256-0.049-0.354-0.146-0.195-0.195-0.195-0.512 0-0.707l3.646-3.646-3.646-3.646c-0.195-0.195-0.195-0.512 0-0.707s0.512-0.195 0.707 0l4 4c0.195 0.195 0.195 0.512 0 0.707l-4 4c-0.098 0.098-0.226 0.146-0.354 0.146z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#0080ff;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M7.5 15c-0.091 0-0.182-0.025-0.265-0.076-0.234-0.146-0.305-0.455-0.159-0.689l5-8c0.146-0.234 0.455-0.305 0.689-0.159s0.305 0.455 0.159 0.689l-5 8c-0.095 0.152-0.258 0.235-0.424 0.235z"
|
||||||
|
id="path6"
|
||||||
|
style="fill:#0080ff;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg6"
|
||||||
|
sodipodi:docname="lnr-coffee-cup.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata12">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs10" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview8"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M10 15c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3-1.346 3-3 3zM10 10c-1.103 0-2 0.897-2 2s0.897 2 2 2c1.103 0 2-0.897 2-2s-0.897-2-2-2z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#141a1f;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M15.904 2.056l-0.177-0.707c-0.189-0.756-0.948-1.349-1.728-1.349h-8c-0.78 0-1.538 0.593-1.728 1.349l-0.177 0.707c-0.631 0.177-1.096 0.757-1.096 1.444v1c0 0.663 0.432 1.226 1.029 1.424l0.901 12.614c0.058 0.806 0.762 1.462 1.57 1.462h7c0.808 0 1.512-0.656 1.57-1.462l0.901-12.614c0.597-0.198 1.029-0.761 1.029-1.424v-1c0-0.687-0.464-1.267-1.096-1.444zM6 1h8c0.319 0 0.68 0.282 0.757 0.591l0.102 0.409h-9.719l0.102-0.409c0.077-0.309 0.438-0.591 0.757-0.591zM14.892 7h-9.783l-0.071-1h9.926l-0.071 1zM14.249 16h-8.497l-0.571-8h9.64l-0.571 8zM13.5 19h-7c-0.29 0-0.552-0.244-0.573-0.533l-0.105-1.467h8.355l-0.105 1.467c-0.021 0.289-0.283 0.533-0.573 0.533zM16 4.5c0 0.276-0.224 0.5-0.5 0.5h-11c-0.276 0-0.5-0.224-0.5-0.5v-1c0-0.275 0.224-0.499 0.499-0.5 0.001 0 0.001 0 0.002 0s0.002-0 0.003-0h10.997c0.276 0 0.5 0.224 0.5 0.5v1z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#141a1f;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="lnr-graduation-hat.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M18.658 7.026l-9-3c-0.103-0.034-0.214-0.034-0.316 0l-9 3c-0.204 0.068-0.342 0.259-0.342 0.474s0.138 0.406 0.342 0.474l2.658 0.886v2.64c0 0.133 0.053 0.26 0.146 0.354 0.088 0.088 2.194 2.146 6.354 2.146 1.513 0 2.924-0.272 4.195-0.809 0.254-0.107 0.373-0.401 0.266-0.655s-0.401-0.373-0.655-0.266c-1.147 0.485-2.427 0.73-3.805 0.73-1.945 0-3.376-0.504-4.234-0.926-0.635-0.313-1.060-0.629-1.266-0.799v-2.081l5.342 1.781c0.051 0.017 0.105 0.026 0.158 0.026s0.107-0.009 0.158-0.026l5.342-1.781v0.892c-0.582 0.206-1 0.762-1 1.414 0 0.611 0.367 1.137 0.892 1.371l-0.877 3.508c-0.037 0.149-0.004 0.308 0.091 0.429s0.24 0.192 0.394 0.192h2c0.154 0 0.299-0.071 0.394-0.192s0.128-0.28 0.091-0.429l-0.877-3.508c0.525-0.234 0.892-0.76 0.892-1.371 0-0.652-0.418-1.208-1-1.414v-1.226l2.658-0.886c0.204-0.068 0.342-0.259 0.342-0.474s-0.138-0.406-0.342-0.474zM15.5 11c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5-0.5-0.224-0.5-0.5 0.224-0.5 0.5-0.5zM15.14 16l0.36-1.438 0.36 1.438h-0.719zM15.46 7.986l-5.877-0.98c-0.273-0.045-0.53 0.139-0.575 0.411s0.139 0.53 0.411 0.575l4.014 0.669-3.932 1.311-7.419-2.473 7.419-2.473 7.419 2.473-1.459 0.486z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#28a745;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="lnr-highlight.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M19.854 9.646c-0.195-0.195-0.512-0.195-0.707 0l-3.586 3.586c-0.585 0.585-1.537 0.585-2.121 0l-4.672-4.672c-0.282-0.282-0.437-0.658-0.437-1.061s0.155-0.779 0.437-1.061l3.586-3.586c0.195-0.195 0.195-0.512 0-0.707s-0.512-0.195-0.707 0l-3.586 3.586c-0.471 0.471-0.73 1.098-0.73 1.768 0 0.285 0.048 0.563 0.138 0.824l-7.322 7.322c-0.094 0.094-0.146 0.221-0.146 0.354v1.5c0 0.276 0.224 0.5 0.5 0.5h9.5c0.133 0 0.26-0.053 0.354-0.146l3.322-3.322c0.261 0.091 0.539 0.138 0.824 0.138 0.669 0 1.297-0.259 1.768-0.73l3.586-3.586c0.195-0.195 0.195-0.512 0-0.707zM9.793 17h-8.793v-0.793l7.002-7.002c0.020 0.021 0.039 0.042 0.059 0.062l4.672 4.672c0.020 0.020 0.041 0.040 0.062 0.059l-3.002 3.002z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ffad0a;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="lnr-layers.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata14">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs12" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg8" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M10 12c-0.066 0-0.132-0.013-0.194-0.039l-9.5-4c-0.185-0.078-0.306-0.26-0.306-0.461s0.121-0.383 0.306-0.461l9.5-4c0.124-0.052 0.264-0.052 0.388 0l9.5 4c0.185 0.078 0.306 0.26 0.306 0.461s-0.121 0.383-0.306 0.461l-9.5 4c-0.062 0.026-0.128 0.039-0.194 0.039zM1.788 7.5l8.212 3.457 8.212-3.457-8.212-3.457-8.212 3.457z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#00a98f;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M10 15c-0.066 0-0.132-0.013-0.194-0.039l-9.5-4c-0.254-0.107-0.374-0.4-0.267-0.655s0.4-0.374 0.655-0.267l9.306 3.918 9.306-3.918c0.254-0.107 0.548 0.012 0.655 0.267s-0.012 0.548-0.267 0.655l-9.5 4c-0.062 0.026-0.128 0.039-0.194 0.039z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#00a98f;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M10 18c-0.066 0-0.132-0.013-0.194-0.039l-9.5-4c-0.254-0.107-0.374-0.4-0.267-0.655s0.4-0.374 0.655-0.267l9.306 3.918 9.306-3.918c0.254-0.107 0.548 0.012 0.655 0.267s-0.012 0.548-0.267 0.655l-9.5 4c-0.062 0.026-0.128 0.039-0.194 0.039z"
|
||||||
|
id="path6"
|
||||||
|
style="fill:#00a98f;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="lnr-heart-pulse.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata14">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs12" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg8" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M9.5 19c-0.084 0-0.167-0.021-0.243-0.063-0.116-0.065-2.877-1.611-5.369-4.082-0.196-0.194-0.197-0.511-0.003-0.707s0.511-0.197 0.707-0.003c1.979 1.962 4.186 3.346 4.908 3.776 0.723-0.431 2.932-1.817 4.908-3.776 0.196-0.194 0.513-0.193 0.707 0.003s0.193 0.513-0.003 0.707c-2.493 2.471-5.253 4.017-5.369 4.082-0.076 0.042-0.159 0.063-0.243 0.063z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ff5c75;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M1.279 11c-0.188 0-0.368-0.106-0.453-0.287-0.548-1.165-0.826-2.33-0.826-3.463 0-2.895 2.355-5.25 5.25-5.25 0.98 0 2.021 0.367 2.931 1.034 0.532 0.39 0.985 0.86 1.319 1.359 0.334-0.499 0.787-0.969 1.319-1.359 0.91-0.667 1.951-1.034 2.931-1.034 2.895 0 5.25 2.355 5.25 5.25 0 1.133-0.278 2.298-0.826 3.463-0.118 0.25-0.415 0.357-0.665 0.24s-0.357-0.415-0.24-0.665c0.485-1.031 0.731-2.053 0.731-3.037 0-2.343-1.907-4.25-4.25-4.25-1.703 0-3.357 1.401-3.776 2.658-0.068 0.204-0.259 0.342-0.474 0.342s-0.406-0.138-0.474-0.342c-0.419-1.257-2.073-2.658-3.776-2.658-2.343 0-4.25 1.907-4.25 4.25 0 0.984 0.246 2.006 0.731 3.037 0.118 0.25 0.010 0.548-0.24 0.665-0.069 0.032-0.141 0.048-0.212 0.048z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#ff5c75;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M10.515 15c-0.005 0-0.009-0-0.013-0-0.202-0.004-0.569-0.109-0.753-0.766l-1.217-4.334-0.807 3.279c-0.158 0.643-0.525 0.778-0.73 0.8s-0.592-0.027-0.889-0.62l-0.606-1.211c-0.029-0.058-0.056-0.094-0.076-0.117-0.003 0.004-0.007 0.009-0.011 0.015-0.37 0.543-1.192 0.953-1.913 0.953h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.421 0 0.921-0.272 1.087-0.516 0.223-0.327 0.547-0.501 0.891-0.478 0.374 0.025 0.708 0.279 0.917 0.696l0.445 0.89 0.936-3.803c0.158-0.64 0.482-0.779 0.726-0.783s0.572 0.125 0.751 0.76l1.284 4.576 1.178-3.608c0.205-0.628 0.582-0.736 0.788-0.745s0.59 0.068 0.847 0.677l0.724 1.719c0.136 0.322 0.578 0.616 0.927 0.616h1.5c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5h-1.5c-0.747 0-1.559-0.539-1.849-1.228l-0.592-1.406-1.274 3.9c-0.207 0.634-0.566 0.733-0.771 0.733z"
|
||||||
|
id="path6"
|
||||||
|
style="fill:#ff5c75;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg12"
|
||||||
|
sodipodi:docname="lnr-store.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata18">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs16" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview14"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg12" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M18 9.5v-1c0-0.078-0.018-0.154-0.053-0.224l-2-4c-0.085-0.169-0.258-0.276-0.447-0.276h-12c-0.189 0-0.363 0.107-0.447 0.276l-2 4c-0.035 0.069-0.053 0.146-0.053 0.224v1c0 0.816 0.393 1.542 1 1.999v6.501h-0.5c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h16c0.276 0 0.5-0.224 0.5-0.5s-0.224-0.5-0.5-0.5h-0.5v-6.501c0.607-0.457 1-1.182 1-1.999zM11.5 11c-0.827 0-1.5-0.673-1.5-1.5v-0.5h3v0.5c0 0.827-0.673 1.5-1.5 1.5zM2 9.5v-0.5h3v0.5c0 0.827-0.673 1.5-1.5 1.5s-1.5-0.673-1.5-1.5zM9 5v3h-2.86l0.75-3h2.11zM12.11 5l0.75 3h-2.86v-3h2.11zM6 9h3v0.5c0 0.827-0.673 1.5-1.5 1.5s-1.5-0.673-1.5-1.5v-0.5zM14 9h3v0.5c0 0.827-0.673 1.5-1.5 1.5s-1.5-0.673-1.5-1.5v-0.5zM16.691 8h-2.801l-0.75-3h2.051l1.5 3zM3.809 5h2.051l-0.75 3h-2.801l1.5-3zM3 11.95c0.162 0.033 0.329 0.050 0.5 0.050 0.817 0 1.544-0.394 2-1.002 0.456 0.608 1.183 1.002 2 1.002s1.544-0.394 2-1.002c0.361 0.48 0.89 0.827 1.5 0.951v6.050h-8v-6.050zM16 18h-4v-6.050c0.61-0.124 1.139-0.471 1.5-0.951 0.456 0.608 1.183 1.002 2 1.002 0.171 0 0.338-0.017 0.5-0.050v6.050z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#17a2b8;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M14 14.5c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5c0-0.276 0.224-0.5 0.5-0.5s0.5 0.224 0.5 0.5z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#17a2b8;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M4.5 15c-0.128 0-0.256-0.049-0.354-0.146-0.195-0.195-0.195-0.512 0-0.707l1-1c0.195-0.195 0.512-0.195 0.707 0s0.195 0.512 0 0.707l-1 1c-0.098 0.098-0.226 0.146-0.354 0.146z"
|
||||||
|
id="path6"
|
||||||
|
style="fill:#17a2b8;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M5.5 17c-0.128 0-0.256-0.049-0.354-0.146-0.195-0.195-0.195-0.512 0-0.707l3-3c0.195-0.195 0.512-0.195 0.707 0s0.195 0.512 0 0.707l-3 3c-0.098 0.098-0.226 0.146-0.354 0.146z"
|
||||||
|
id="path8"
|
||||||
|
style="fill:#17a2b8;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M8.5 17c-0.128 0-0.256-0.049-0.354-0.146-0.195-0.195-0.195-0.512 0-0.707l1-1c0.195-0.195 0.512-0.195 0.707 0s0.195 0.512 0 0.707l-1 1c-0.098 0.098-0.226 0.146-0.354 0.146z"
|
||||||
|
id="path10"
|
||||||
|
style="fill:#17a2b8;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="lnr-tablet.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata14">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs12" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg8" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M16.5 20h-13c-0.827 0-1.5-0.673-1.5-1.5v-17c0-0.827 0.673-1.5 1.5-1.5h13c0.827 0 1.5 0.673 1.5 1.5v17c0 0.827-0.673 1.5-1.5 1.5zM3.5 1c-0.276 0-0.5 0.224-0.5 0.5v17c0 0.276 0.224 0.5 0.5 0.5h13c0.276 0 0.5-0.224 0.5-0.5v-17c0-0.276-0.224-0.5-0.5-0.5h-13z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ab47bc;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M10.5 18h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#ab47bc;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M15.5 16h-11c-0.276 0-0.5-0.224-0.5-0.5v-13c0-0.276 0.224-0.5 0.5-0.5h11c0.276 0 0.5 0.224 0.5 0.5v13c0 0.276-0.224 0.5-0.5 0.5zM5 15h10v-12h-10v12z"
|
||||||
|
id="path6"
|
||||||
|
style="fill:#ab47bc;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="lnr-lock.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M14.5 8h-0.5v-1.5c0-2.481-2.019-4.5-4.5-4.5s-4.5 2.019-4.5 4.5v1.5h-0.5c-0.827 0-1.5 0.673-1.5 1.5v8c0 0.827 0.673 1.5 1.5 1.5h10c0.827 0 1.5-0.673 1.5-1.5v-8c0-0.827-0.673-1.5-1.5-1.5zM6 6.5c0-1.93 1.57-3.5 3.5-3.5s3.5 1.57 3.5 3.5v1.5h-7v-1.5zM15 17.5c0 0.276-0.224 0.5-0.5 0.5h-10c-0.276 0-0.5-0.224-0.5-0.5v-8c0-0.276 0.224-0.5 0.5-0.5h10c0.276 0 0.5 0.224 0.5 0.5v8z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#0080ff;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
id="svg6"
|
||||||
|
sodipodi:docname="lnr-select.svg"
|
||||||
|
inkscape:version="0.92.4 (unknown)">
|
||||||
|
<metadata
|
||||||
|
id="metadata12">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs10" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1004"
|
||||||
|
id="namedview8"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.8"
|
||||||
|
inkscape:cx="10"
|
||||||
|
inkscape:cy="10"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M5.5 15h-4c-0.827 0-1.5-0.673-1.5-1.5v-12c0-0.827 0.673-1.5 1.5-1.5h14c0.827 0 1.5 0.673 1.5 1.5v7c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-7c0-0.276-0.224-0.5-0.5-0.5h-14c-0.276 0-0.5 0.224-0.5 0.5v12c0 0.276 0.224 0.5 0.5 0.5h4c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#0080df;fill-opacity:0.94117647" />
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M13 20c-0.198 0-0.386-0.119-0.464-0.314l-1.697-4.242-2.963 3.386c-0.137 0.157-0.357 0.212-0.552 0.139s-0.324-0.26-0.324-0.468v-15c0-0.198 0.117-0.377 0.298-0.457s0.392-0.046 0.539 0.087l11 10c0.153 0.139 0.205 0.358 0.13 0.55s-0.26 0.32-0.466 0.32h-4.261l1.726 4.314c0.103 0.256-0.022 0.547-0.279 0.65l-2.5 1c-0.061 0.024-0.124 0.036-0.186 0.036zM11 14c0.028 0 0.056 0.002 0.084 0.007 0.172 0.029 0.315 0.146 0.38 0.307l1.814 4.536 1.572-0.629-1.814-4.536c-0.062-0.154-0.043-0.329 0.050-0.466s0.248-0.22 0.414-0.22h3.707l-9.207-8.37v12.539l2.624-2.999c0.096-0.109 0.233-0.171 0.376-0.171z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#0080df;fill-opacity:0.94117647" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
|
@ -0,0 +1,313 @@
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- Navbar START-->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light" style="background: #fff !important;margin: 30px -10px;border-radius: 4px;overflow: hidden;box-shadow: 0 1px 2px 0px rgba(0, 0, 0, 0.16), 0 0px 2px rgba(27, 26, 26, 0.34);padding: 0px;">
|
||||||
|
<a class="navbar-brand" href="https://www.droggol.com" target="_blank" style="background: #374e65;position: absolute;padding: 13px 20px 11px 20px;left: -1px">
|
||||||
|
<img style="max-width: 90px;margin-bottom: 6px;" src="images/logo.svg"/>
|
||||||
|
</a>
|
||||||
|
<div class="ml-auto text-right" id="navbarNav">
|
||||||
|
<ul class="nav navbar-nav ml-auto text-right">
|
||||||
|
<li class="nav-item" style="padding: 10px;border-left: 1px solid #d5d3d3;">
|
||||||
|
<span class="badge badge-pill badge-primary" style="background: #7c7bad33;border: 1px solid #7c7bbb4f;font-size: 14px;color: #7c7bad;margin: 3px;padding-left: 23px;position: relative;">
|
||||||
|
<i class="fa fa-check-circle" style="font-size: 14px;position: absolute;left: 5px;top: 2px;font-size: 16px;"></i>
|
||||||
|
Community
|
||||||
|
</span>
|
||||||
|
<span class="badge badge-pill badge-primary" style="background: #eadae5;border: 1px solid #905a7b4a;font-size: 14px;color: #875a7b;margin: 3px;padding-left: 23px;position: relative;">
|
||||||
|
<i class="fa fa-check-circle" style="font-size: 14px;position: absolute;left: 5px;top: 2px;font-size: 16px;"></i>
|
||||||
|
Enterprise
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-md-block">
|
||||||
|
<a href="https://www.droggol.com/contactus" style="padding: 10px;border-left: 1px solid #d5d3d3;color: #0080Ff;padding: 14px 25px;font-weight: bold;font-size: 14px;display: inline-block;" target="_blank">
|
||||||
|
Need Help?
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- Navbar END-->
|
||||||
|
|
||||||
|
<!-- Hero START-->
|
||||||
|
<div class="row pb64 pt32 align-items-center">
|
||||||
|
<div class="col-12 col-md-7 col-lg-6">
|
||||||
|
<h2 style="color: #3C4858;">
|
||||||
|
<span style="color: #0080FF;"> Double click </span>
|
||||||
|
<span style="font-weight: 300;"> to edit/save form view. </span>
|
||||||
|
</h2>
|
||||||
|
<p style="color: #8492A6;" class="lead mb-4">
|
||||||
|
This module provides the feature to switch between edit/save mode form view just with double click.
|
||||||
|
</p>
|
||||||
|
<div class="media">
|
||||||
|
<div class="d-flex mr-4">
|
||||||
|
<div style="background-color: rgba(0, 128, 255, 0.1);" class="badge border-0 rounded-circle p-3">
|
||||||
|
<img src="images/select.svg" style="height:2em; height: 2em;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media-body">
|
||||||
|
<h5 style="color: #3C4858;">Simple to use</h5>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">Just double click on empty area of from view to edit/save the
|
||||||
|
form view. </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media">
|
||||||
|
<div class="d-flex mr-4">
|
||||||
|
<div style="background-color: rgba(0, 128, 255, 0.1);" class="badge border-0 rounded-circle p-3">
|
||||||
|
<img src="images/lock.svg" style="height:2em; width: 2em;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media-body">
|
||||||
|
<h5 style="color: #3C4858;">Based On Access Rules</h5>
|
||||||
|
<p style="color: #8492A6;" class="mb-5 mb-md-0">
|
||||||
|
This module does not allow to switch into editable mode if user don't have access rights.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-5 col-lg-6">
|
||||||
|
<div style="perspective-origin: left center; perspective: 1500px; transform-style: preserve-3d;">
|
||||||
|
<img src="images/screen_shot.png" class="img-fluid"
|
||||||
|
style="border: 1px solid rgba(143, 155, 174, 0.31);transform: rotateY(-35deg) rotateX(15deg) !important; border-radius: .625rem; box-shadow: 25px 60px 125px -25px rgba(80,102,144,.1), 16px 40px 75px -40px rgba(0,0,0,.2);">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Hero END-->
|
||||||
|
|
||||||
|
<!-- Info START-->
|
||||||
|
<div class="row text-center pb64 pt64 bg-100">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 style="color: #3C4858;" class="mt-2 mb-3">How to use it?</h2>
|
||||||
|
<p style="color: #6b7789;font-weight: 300;" class="lead">
|
||||||
|
After installing this module, whenever you open any
|
||||||
|
form view. You can switch between editable and read-only mode just by double-clicking on the empty area. For
|
||||||
|
better usability, we have ignored double clicks on fields so you can select the text by double or triple
|
||||||
|
clicks without any issue. The user can not use this feature if he/she doesn't have access rights to edit
|
||||||
|
that record.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="https://www.youtube.com/watch?v=UZ5CwVPe6d8" style="background-color: #0080FF;border-color: #0080FF;border-radius: 0.375rem;line-height: 1.65;font-size: 1rem;font-weight: 500;color: #fff;" class="btn btn-lg" target="_blank">
|
||||||
|
<i class="fa fa-youtube-play fa-2x mr-2" style="vertical-align: middle;"></i>
|
||||||
|
<span>See It In Action </span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Info END-->
|
||||||
|
|
||||||
|
<!-- Help START-->
|
||||||
|
<div class="row pt64 pb64 text-center">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 style="color: #3C4858;" class="mt-2 mb-3">Need a help for this module?</h2>
|
||||||
|
<a href="https://www.droggol.com/contactus" target="_blank" style="background-color: #0080FF;border-color: #0080FF;border-radius: 0.375rem;line-height: 1.65;font-size: 1rem;color: #fff;" class="btn btn-lg mr-4">
|
||||||
|
Get Support
|
||||||
|
</a>
|
||||||
|
<a href="https://www.droggol.com/contactus" target="_blank" style="background-color: #0080FF;border-color: #0080FF;border-radius: 0.375rem;line-height: 1.65;font-size: 1rem;color: #fff;" class="btn btn-lg mr-4">
|
||||||
|
Request Customization
|
||||||
|
</a>
|
||||||
|
<a href="https://www.droggol.com/contactus" target="_blank" style="background-color: #0080FF;border-color: #0080FF;border-radius: 0.375rem;line-height: 1.65;font-size: 1rem;color: #fff;" class="btn btn-lg">
|
||||||
|
Contact Us
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Help END-->
|
||||||
|
|
||||||
|
<!-- Cards START-->
|
||||||
|
<div class="row pt16 pb16 bg-100">
|
||||||
|
<div class="col-12 col-md-12 text-center mt-4">
|
||||||
|
<span style="background-color: rgba(0, 128, 255, 0.1); color: #0080FF; padding: 0.6em 0.8em; font-size: 75%; font-weight: 700;" class="badge badge-pill border-0">
|
||||||
|
Our Services
|
||||||
|
</span>
|
||||||
|
<h2 style="color: #3C4858;" class="mt-2">Checkout Our Odoo Services</h2>
|
||||||
|
<p style="color: #737d8b;" class="lead">Great variety of Odoo services that help you to uplift your awesome business.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cards Grid-->
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(0, 128, 255, 0.1);" class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/code.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;"> Development </h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
Customize Odoo modules, integrate with external services, migration from the old version and more.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/odoo-development" target="_blank" style="background-color: rgba(0, 128, 255, 0.1); border-color: transparent; color: #0080FF; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;" class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(0, 169, 143, 0.1);" class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/layers.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;">
|
||||||
|
Implementation
|
||||||
|
</h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
We understand your business needs and setup Odoo in the right way to fulfil your business needs.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/odoo-implementation"target="_blank"style="background-color: rgba(0, 169, 143, 0.1); border-color: transparent; color: #00A98F; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;"class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(171, 71, 188, 0.1); color: #AB47BC;" class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/tablet.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;">
|
||||||
|
Mobile Apps
|
||||||
|
</h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
Android, iOS or Hybrid mobile applications which are fully integrated with Odoo instances.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/odoo-mobile-apps" target="_blank" style="background-color: rgba(171, 71, 188, 0.1); border-color: transparent; color: #AB47BC; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;" class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(255, 173, 10, 0.1);" class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/highlight.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;">
|
||||||
|
Website & Themes
|
||||||
|
</h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
Build eye-catching, blazing fast, configurable Odoo website with beautiful custom themes.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/odoo-website-and-themes" target="_blank" style="background-color: rgba(255, 173, 10, 0.1); border-color: transparent; color: #FFAD0A; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;" class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(255, 92, 117, 0.1);" class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/pulse.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;">
|
||||||
|
Support
|
||||||
|
</h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
Odoo functional or technical support, bug-fixes, debugging, performance optimization, and more.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/odoo-support" target="_blank" style="background-color: rgba(255, 92, 117, 0.1); border-color: transparent; color: #FF5C75; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;" class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(23, 162, 184, 0.1);" class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/store.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;">
|
||||||
|
POS & IoT
|
||||||
|
</h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
Customize Odoo POS for your retail shops and restaurants. Integrate it with hardware devices to
|
||||||
|
boost your productivity.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/odoo-pos-and-iot" target="_blank" style="background-color: rgba(23, 162, 184, 0.1); border-color: transparent; color: #17a2b8; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;" class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(171, 71, 188, 0.1); color: #AB47BC;" class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/cloud.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;">
|
||||||
|
Deployment
|
||||||
|
</h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
Deploy or optimize your Odoo instances on the cloud, Odoo.sh or on any other platform with top-notch
|
||||||
|
security.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/odoo-deployment" target="_blank" style="background-color: rgba(171, 71, 188, 0.1); border-color: transparent; color: #AB47BC; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;" class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(40, 167, 69, 0.1);" class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/hat.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;">
|
||||||
|
Training
|
||||||
|
</h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
Learn to develop or customize Odoo backend, website, JavaScript framework, sysadmin, themes and
|
||||||
|
more.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/odoo-trainings" target="_blank" style="background-color: rgba(40, 167, 69, 0.1); border-color: transparent; color: #28a745; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;" class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 my-3">
|
||||||
|
<div style="background: #fff;" class="card deep-1 deep_hover py-4 h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div style="background-color: rgba(20, 26, 31, 0.1); color: #141a1f;"
|
||||||
|
class="badge border-0 rounded-circle mb-4 p-3">
|
||||||
|
<img src="images/cards/coffee.svg" style="width:3em; height: 3em;" />
|
||||||
|
</div>
|
||||||
|
<h4 style="color: #3C4858;">
|
||||||
|
Hire Developers
|
||||||
|
</h4>
|
||||||
|
<p style="color: #8492A6;" class="mb-4">
|
||||||
|
Hire full-stack Odoo developer to develop, maintain or configure Odoo applications on a regular
|
||||||
|
basis.
|
||||||
|
</p>
|
||||||
|
<a href="https://www.droggol.com/hire-odoo-developers" target="_blank" style="background-color: rgba(20, 26, 31, 0.1); border-color: transparent; color: #141a1f; border-radius: 0.375rem; line-height: 1.65; font-size: 0.75rem; font-weight: 400;" class="btn btn-sm badge border-0 rounded-circle">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-center mt16 mb16">
|
||||||
|
<a href="https://www.droggol.com/services" target="_blank" style="background-color: #0080FF;border-color: #0080FF;border-radius: 0.375rem;line-height: 1.65;font-size: 1rem;font-weight: 500;color: #fff;" class="btn btn-lg" target="_blank">
|
||||||
|
<span> View All Sevices </span>
|
||||||
|
<i class="fa fa-arrow-right mr-2" style="vertical-align: middle;"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Cards END-->
|
||||||
|
|
||||||
|
<!-- CTA END-->
|
||||||
|
<div class="row align-items-center pt32 pb32" style="background-image: url(images/bg.svg);background-color: #0080FF;border-radius: 8px;margin: 20px 0px;padding: 20px;">
|
||||||
|
<div class="col-12 col-md" style="">
|
||||||
|
<h3 style="color: #fff;">Let's have a talk.</h3>
|
||||||
|
<p style="color: #fff;" class="mb-md-0">Don't be a stranger. Our team is happy to answer all your questions.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<a href="https://www.droggol.com/contactus" target="_blank" style="padding: 10px 25px;border-radius: 6px;color: #0080ff;font-weight: 500;background:#fff;" class="btn btn-default">
|
||||||
|
CONTACT US
|
||||||
|
<i class="fa fa-arrow-circle-right fa-lg" style="margin-left:4px;"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- CTA END-->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { FormController } from "@web/views/form/form_controller";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { onMounted, onWillUnmount } from "@odoo/owl";
|
||||||
|
|
||||||
|
patch(FormController.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
|
||||||
|
this._dblClickHandler = this.onDblClick.bind(this);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Add double click event listener to the form
|
||||||
|
if (this.rootRef && this.rootRef.el) {
|
||||||
|
this.rootRef.el.addEventListener('dblclick', this._dblClickHandler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onWillUnmount(() => {
|
||||||
|
// Clean up event listener
|
||||||
|
if (this.rootRef && this.rootRef.el) {
|
||||||
|
this.rootRef.el.removeEventListener('dblclick', this._dblClickHandler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onDblClick(ev) {
|
||||||
|
const target = ev.target;
|
||||||
|
|
||||||
|
// Avoid triggering on chatter, modal, or control panel elements
|
||||||
|
if (target.closest('.o-mail-Chatter') ||
|
||||||
|
target.closest('.o_chatter') ||
|
||||||
|
target.closest('.modal') ||
|
||||||
|
target.closest('.o_control_panel') ||
|
||||||
|
target.closest('.o-mail-ChatWindow')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if click is on a field or input element
|
||||||
|
const isInteractiveElement =
|
||||||
|
target.closest('.o_field_widget') ||
|
||||||
|
target.classList.contains('o_field_widget') ||
|
||||||
|
target.tagName === 'INPUT' ||
|
||||||
|
target.tagName === 'TEXTAREA' ||
|
||||||
|
target.tagName === 'SELECT' ||
|
||||||
|
target.tagName === 'BUTTON' ||
|
||||||
|
target.closest('button') ||
|
||||||
|
target.closest('.dropdown') ||
|
||||||
|
target.closest('.o_dropdown') ||
|
||||||
|
target.closest('.btn');
|
||||||
|
|
||||||
|
const record = this.model.root;
|
||||||
|
const mode = record.mode;
|
||||||
|
|
||||||
|
if (mode === 'readonly' && !isInteractiveElement) {
|
||||||
|
// Switch to edit mode on double click in readonly mode
|
||||||
|
if (!record.isReadonly && this.archInfo.activeActions.edit) {
|
||||||
|
this.discard();
|
||||||
|
this.edit();
|
||||||
|
}
|
||||||
|
} else if (mode === 'edit' && !isInteractiveElement) {
|
||||||
|
// Save the record on double click in edit mode
|
||||||
|
if (record.isDirty) {
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': "Dynamic Reject Workflow",
|
||||||
|
'version': '18.0.1.0.0',
|
||||||
|
'description': """
|
||||||
|
Dynamic Reject Workflow
|
||||||
|
""",
|
||||||
|
'category': 'Odex25-base',
|
||||||
|
'author': "Expert Co. Ltd.",
|
||||||
|
'website': "http://www.exp-sa.com",
|
||||||
|
'depends': ['mail'],
|
||||||
|
'data': [
|
||||||
|
'security/groups.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/reject_buttons.xml',
|
||||||
|
'views/reject_workflow.xml',
|
||||||
|
'wizard/reject_wizard.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'dynamic_reject_workflow/static/src/js/form.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -0,0 +1,332 @@
|
||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * dynamic_reject_workflow
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 14.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2022-09-13 07:10+0000\n"
|
||||||
|
"PO-Revision-Date: 2022-09-13 07:10+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__button_name
|
||||||
|
msgid "Button Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__name
|
||||||
|
msgid "Button String"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: code:addons/dynamic_reject_workflow/models/reject_workflow.py:0
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.view_form_reject_button_wizard
|
||||||
|
#, python-format
|
||||||
|
msgid "Buttons"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.reject_wizard_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.view_form_reject_button_wizard
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "إلغـــاء"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.reject_workflow_form
|
||||||
|
msgid "Choose Buttons"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.reject_wizard_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.view_form_reject_button_wizard
|
||||||
|
msgid "Send"
|
||||||
|
msgstr "إرســال"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button_wizard__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_wizard__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__create_uid
|
||||||
|
msgid "Created by"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__create_date
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button_wizard__create_date
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_wizard__create_date
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__create_date
|
||||||
|
msgid "Created on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_thread__display_name
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__display_name
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button_wizard__display_name
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_wizard__display_name
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__display_name
|
||||||
|
msgid "Display Name"
|
||||||
|
msgstr "الاسم المعروض"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model,name:dynamic_reject_workflow.model_mail_thread
|
||||||
|
msgid "Email Thread"
|
||||||
|
msgstr "المحادثة البريدية"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_thread__id
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__id
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button_wizard__id
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_wizard__id
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__id
|
||||||
|
msgid "ID"
|
||||||
|
msgstr "المُعرف"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__invisible
|
||||||
|
msgid "Invisible"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_thread____last_update
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button____last_update
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button_wizard____last_update
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_wizard____last_update
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow____last_update
|
||||||
|
msgid "Last Modified on"
|
||||||
|
msgstr "آخر تعديل في"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button_wizard__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_wizard__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__write_uid
|
||||||
|
msgid "Last Updated by"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__write_date
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button_wizard__write_date
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_wizard__write_date
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__write_date
|
||||||
|
msgid "Last Updated on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__model_id
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__model_id
|
||||||
|
msgid "Model Ref."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__name
|
||||||
|
msgid "Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_internal_transaction__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_outgoing_transaction__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_salary_advance__reason
|
||||||
|
msgid "Reason"
|
||||||
|
msgstr "السبب"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_analytic_account__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_asset__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_asset_modify__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_asset_multi_operation__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_asset_operation__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_bank_statement__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_bank_statement_import_journal_creation__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_bank_statement_line__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_fiscal_year__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_journal__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_loan__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_move__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_online_link__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_online_provider__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_payment__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_payment_method__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_payment_term__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_account_tax__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_appraisal_degree__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_appraisal_plan__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_asset_pause__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_asset_sell__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_budget_confirmation__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_calendar_event__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_company_document__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_consolidation_period__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_contract_advantage__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_crm_lead__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_crm_team__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_crossovered_budget__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_customize_appraisal__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_checklist__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_course_name__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_department_jobs__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_effective_form__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_iqama_renewal__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_leave__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_other_request__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_overtime_request__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_employee_promotions__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_event_event__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_event_registration__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_fiscalyears_periods__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_fleet_vehicle__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_fleet_vehicle_log_contract__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_fleet_vehicle_log_services__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_form_renew__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_forum_forum__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_forum_post__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_forum_tag__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_gamification_badge__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_gamification_challenge__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_administrative_circular__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_applicant__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_attendance_report__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_clearance_form__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_contract__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_contract_extension__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_deduction__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_department__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_employee_appraisal__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_employee_reward__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_exit_return__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_expense__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_expense_sheet__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_group_employee_appraisal__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_holiday_officials__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_holidays__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_holidays_restriction__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_job__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_loan_payment_suspension__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_loan_salary_advance__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_official_mission__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_official_mission_employee__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_payroll_nomination__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_payroll_promotion_setting__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_payroll_raise__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_penalty_register__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_penalty_ss__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_personal_permission__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_punishment__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_re_contract__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_renew_official_paper__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_request_visa__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_termination__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_termination_patch__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_hr_ticket_request__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_leave_cancellation__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_blacklist__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_channel__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_thread__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_thread_blacklist__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_thread_cc__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mail_thread_phone__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mailing_contact__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mailing_mailing__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_maintenance_equipment__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_maintenance_equipment_category__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_maintenance_request__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_manager_appraisal_line__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_mission_destination__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_note_note__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_petty_cash__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_phone_blacklist__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_product_product__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_product_template__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_project_project__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_project_task__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_purchase_order__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_purchase_requisition__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reconcile_leaves__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_wizard__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_res_currency__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_res_partner__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_res_users__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_return_from_leave__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_sale_order__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_stock_inventory__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_stock_picking__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_stock_production_lot__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_stock_scrap__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_survey_survey__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_ticket_class__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_all_reject_wizard__reason
|
||||||
|
msgid "Reason/Justification"
|
||||||
|
msgstr "السبب/التوضيح"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: code:addons/dynamic_reject_workflow/models/reject_workflow.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "Reject"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model,name:dynamic_reject_workflow.model_reject_button
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button_wizard__button_ids
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_workflow__button_ids
|
||||||
|
msgid "Reject Button"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model,name:dynamic_reject_workflow.model_reject_button_wizard
|
||||||
|
msgid "Reject Button Wizard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_fleet_maintenance__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_incoming_transaction__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_transaction_transaction__reason
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_vehicle_delegation__reason
|
||||||
|
msgid "Reject Reason"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model,name:dynamic_reject_workflow.model_reject_wizard
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.reject_wizard_view
|
||||||
|
msgid "Reason Wizard"
|
||||||
|
msgstr "السبب"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model,name:dynamic_reject_workflow.model_all_reject_wizard
|
||||||
|
msgid "All Reject Wizard"
|
||||||
|
msgstr "الأسبــاب"
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.actions.act_window,name:dynamic_reject_workflow.reject_workflow_action
|
||||||
|
#: model:ir.model,name:dynamic_reject_workflow.model_reject_workflow
|
||||||
|
#: model:ir.ui.menu,name:dynamic_reject_workflow.menu_reject_workflow
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.reject_workflow_form
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.workflow_stage_tree
|
||||||
|
msgid "Reject Workflow"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:res.groups,name:dynamic_reject_workflow.reject_group_manager
|
||||||
|
msgid "Reject Workflow Manager"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model:ir.model.fields,field_description:dynamic_reject_workflow.field_reject_button__states
|
||||||
|
msgid "States"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: code:addons/dynamic_reject_workflow/models/reject_workflow.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "mail.threadreasonstring"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: dynamic_reject_workflow
|
||||||
|
#: model_terms:ir.ui.view,arch_db:dynamic_reject_workflow.view_form_reject_button_wizard
|
||||||
|
msgid "or"
|
||||||
|
msgstr ""
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import reject_workflow
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
import json
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class RejectWorkflow(models.Model):
|
||||||
|
_name = 'reject.workflow'
|
||||||
|
_description = 'Reject Workflow'
|
||||||
|
|
||||||
|
name = fields.Char(string='Name')
|
||||||
|
model_id = fields.Many2one('ir.model', string='Model Ref.', domain="[('is_mail_thread','=',True)]")
|
||||||
|
button_ids = fields.Many2many('reject.button', string="Reject Button")
|
||||||
|
|
||||||
|
@api.onchange('model_id')
|
||||||
|
def onchange_model_id(self):
|
||||||
|
self.env['reject.button'].search([]).unlink()
|
||||||
|
|
||||||
|
def reject_workflow_buttons(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.model_id:
|
||||||
|
self.env['reject.button'].search([]).unlink()
|
||||||
|
# In Odoo 18, use get_view instead of fields_view_get
|
||||||
|
view_info = self.env[rec.model_id.model].get_view(view_type='form')
|
||||||
|
arch = etree.XML(view_info['arch'])
|
||||||
|
for btn in arch.xpath("//form/header/button"):
|
||||||
|
attr = btn.attrib
|
||||||
|
# Handle modifiers if present
|
||||||
|
modifiers = {}
|
||||||
|
if 'modifiers' in attr:
|
||||||
|
modifiers = json.loads(attr['modifiers'])
|
||||||
|
btn_dict = {
|
||||||
|
'name': attr.get('string'),
|
||||||
|
'button_name': attr.get('name'),
|
||||||
|
'model_id': rec.model_id.id,
|
||||||
|
'invisible': 'invisible' in modifiers and
|
||||||
|
modifiers['invisible'] or False,
|
||||||
|
'states': 'states' in attr and attr['states'] or False
|
||||||
|
}
|
||||||
|
self.env['reject.button'].create(btn_dict)
|
||||||
|
return {
|
||||||
|
'name': _('Buttons'),
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_model': 'reject.button.wizard',
|
||||||
|
'target': 'new',
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_reject_workflow(self, model=None):
|
||||||
|
button_ids = []
|
||||||
|
if model:
|
||||||
|
reject_button = self.search([('model_id.model', '=', model)])
|
||||||
|
button_ids = [{'name': item.button_name, 'string': item.name,
|
||||||
|
'invisible': item.invisible, 'states': item.states}
|
||||||
|
for item in reject_button.button_ids]
|
||||||
|
return button_ids
|
||||||
|
|
||||||
|
|
||||||
|
class RejectButtonWizard(models.TransientModel):
|
||||||
|
_name = 'reject.button.wizard'
|
||||||
|
_description = 'Reject Button Wizard'
|
||||||
|
|
||||||
|
button_ids = fields.Many2many('reject.button', string="Reject Button")
|
||||||
|
|
||||||
|
def confirm(self):
|
||||||
|
active_id = self.env['reject.workflow'].browse(self.env.context.get('active_id'))
|
||||||
|
active_id.write({
|
||||||
|
"button_ids": [(6, 0, self.button_ids.ids)]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class RejectButton(models.Model):
|
||||||
|
_name = 'reject.button'
|
||||||
|
_description = 'Reject Button'
|
||||||
|
|
||||||
|
name = fields.Char(string='Button String')
|
||||||
|
button_name = fields.Char(string='Button Name')
|
||||||
|
invisible = fields.Char(string='Invisible')
|
||||||
|
states = fields.Char(string='States')
|
||||||
|
model_id = fields.Many2one('ir.model', string='Model Ref.')
|
||||||
|
|
||||||
|
|
||||||
|
class MailThread(models.AbstractModel):
|
||||||
|
_inherit = 'mail.thread'
|
||||||
|
|
||||||
|
reason = fields.Text(string='Reason/Justification', tracking=True )
|
||||||
|
|
||||||
|
def action_reject_workflow(self):
|
||||||
|
return {
|
||||||
|
'name': _('Reject'),
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_model': 'all.reject.wizard',
|
||||||
|
'target': 'new',
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.returns('mail.message', lambda value: value.id)
|
||||||
|
def message_post(self, *, body='', subject=None, message_type='notification',
|
||||||
|
parent_id=False, subtype_xmlid=None, attachments=None, **kwargs):
|
||||||
|
context = self.env.context
|
||||||
|
if 'merge_reason' in context:
|
||||||
|
tracking_value_ids = kwargs['tracking_value_ids']
|
||||||
|
tracking_value_ids.append([0, 0, {
|
||||||
|
'field': 'reason',
|
||||||
|
'field_desc': _(self.env['mail.thread'].fields_get('reason')['reason']['string']),
|
||||||
|
'field_type': 'Text',
|
||||||
|
'old_value_char': '',
|
||||||
|
'new_value_char': self.reason
|
||||||
|
}])
|
||||||
|
res = super(MailThread, self).message_post(body=body, subject=subject, message_type=message_type,
|
||||||
|
subtype_xmlid=subtype_xmlid,
|
||||||
|
parent_id=parent_id, attachments=attachments, **kwargs)
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="reject_group_manager" model="res.groups">
|
||||||
|
<field name="category_id" ref="base.module_category_administration"/>
|
||||||
|
<field name="name">Reject Workflow Manager</field>
|
||||||
|
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_reject_workflow_manager,reject_workflow_manager,model_reject_workflow,reject_group_manager,1,1,1,1
|
||||||
|
access_all_reject_wizard_manager,all_reject_wizard_manager,model_all_reject_wizard,reject_group_manager,1,1,1,1
|
||||||
|
access_reject_button,reject_button,model_reject_button,base.group_user,1,1,1,1
|
||||||
|
access_reject_workflow,reject_workflow,model_reject_workflow,base.group_user,1,0,0,0
|
||||||
|
access_reject_button_wizard_manager,reject_button_wizard_manager,model_reject_button_wizard,reject_group_manager,1,1,1,1
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { FormRenderer } from "@web/views/form/form_renderer";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { onWillStart, useState } from "@odoo/owl";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
patch(FormRenderer.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
this.orm = useService("orm");
|
||||||
|
this.state = useState({
|
||||||
|
rejectButtons: []
|
||||||
|
});
|
||||||
|
|
||||||
|
onWillStart(async () => {
|
||||||
|
await this.loadRejectButtons();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadRejectButtons() {
|
||||||
|
if (!this.props.record || !this.props.record.resModel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rejectButtons = await this.orm.call(
|
||||||
|
'reject.workflow',
|
||||||
|
'check_reject_workflow',
|
||||||
|
[this.props.record.resModel],
|
||||||
|
{ model: this.props.record.resModel }
|
||||||
|
);
|
||||||
|
this.state.rejectButtons = rejectButtons || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading reject workflow buttons:', error);
|
||||||
|
this.state.rejectButtons = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
compileButton(buttonNode) {
|
||||||
|
// Call parent method first
|
||||||
|
const result = super.compileButton(buttonNode);
|
||||||
|
|
||||||
|
// Process reject workflow buttons
|
||||||
|
if (this.state.rejectButtons && this.state.rejectButtons.length > 0) {
|
||||||
|
this._processRejectButton(buttonNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
_processRejectButton(buttonNode) {
|
||||||
|
if (!buttonNode || !buttonNode.getAttribute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrs = {
|
||||||
|
name: buttonNode.getAttribute('name'),
|
||||||
|
string: buttonNode.getAttribute('string'),
|
||||||
|
states: buttonNode.getAttribute('states'),
|
||||||
|
invisible: buttonNode.getAttribute('invisible')
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const btn of this.state.rejectButtons) {
|
||||||
|
const nodeInvisible = JSON.stringify(attrs.invisible || '').replace(/[^A-Z0-9]+/ig, "");
|
||||||
|
const btnInvisible = (btn.invisible || '').replace(/[^A-Z0-9]+/ig, "");
|
||||||
|
|
||||||
|
if (btn.name === attrs.name &&
|
||||||
|
btn.string === attrs.string &&
|
||||||
|
btn.states === attrs.states &&
|
||||||
|
nodeInvisible === btnInvisible) {
|
||||||
|
|
||||||
|
// Get existing context
|
||||||
|
let context = {};
|
||||||
|
const contextAttr = buttonNode.getAttribute('context');
|
||||||
|
if (contextAttr) {
|
||||||
|
try {
|
||||||
|
context = JSON.parse(contextAttr);
|
||||||
|
} catch (e) {
|
||||||
|
context = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add record reject name to context
|
||||||
|
context.record_reject_name = attrs.name;
|
||||||
|
|
||||||
|
// Update button attributes
|
||||||
|
buttonNode.setAttribute('context', JSON.stringify(context));
|
||||||
|
buttonNode.setAttribute('name', 'action_reject_workflow');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="view_form_reject_button_wizard" model="ir.ui.view">
|
||||||
|
<field name="name">reject.button.wizard.form</field>
|
||||||
|
<field name="model">reject.button.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Buttons">
|
||||||
|
<field name="button_ids" nolable="1" widget="many2many_checkboxes"/>
|
||||||
|
<footer>
|
||||||
|
<button name="confirm" string="Confirm" type="object" class="oe_highlight"/>
|
||||||
|
or
|
||||||
|
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="reject_workflow_form" model="ir.ui.view">
|
||||||
|
<field name="name">reject.workflow.form</field>
|
||||||
|
<field name="model">reject.workflow</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Reject Workflow">
|
||||||
|
<sheet>
|
||||||
|
<header>
|
||||||
|
<button name="reject_workflow_buttons" string="Choose Buttons"
|
||||||
|
class="btn-primary" type="object"/>
|
||||||
|
</header>
|
||||||
|
<group>
|
||||||
|
<field name="name" required="1" />
|
||||||
|
<field name="model_id" required="1" />
|
||||||
|
<field name="button_ids" force_save="1" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="workflow_stage_tree" model="ir.ui.view">
|
||||||
|
<field name="name">reject.workflow.tree</field>
|
||||||
|
<field name="model">reject.workflow</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Reject Workflow">
|
||||||
|
<field name="name" />
|
||||||
|
<field name="model_id" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="reject_workflow_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Reject Workflow</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">reject.workflow</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="menu_reject_workflow"
|
||||||
|
name="Reject Workflow"
|
||||||
|
parent="base.next_id_2"
|
||||||
|
sequence="100"
|
||||||
|
groups="dynamic_reject_workflow.reject_group_manager"
|
||||||
|
action="reject_workflow_action"/>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import reject_wizard
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class RejectWizard(models.TransientModel):
|
||||||
|
_name = 'all.reject.wizard'
|
||||||
|
_description = 'All Reject Wizard'
|
||||||
|
|
||||||
|
reason = fields.Text(string='Reason/Justification')
|
||||||
|
|
||||||
|
def button_confirm(self):
|
||||||
|
context = dict(self._context)
|
||||||
|
active_model = context.get('active_model')
|
||||||
|
active_id = context.get('active_id')
|
||||||
|
if 'record_reject_name' in context:
|
||||||
|
rec_id = self.env[active_model].browse(active_id)
|
||||||
|
rec_id.write({
|
||||||
|
'reason': self.reason
|
||||||
|
})
|
||||||
|
reject_func = getattr(rec_id.with_context({'merge_reason': True}), context.get('record_reject_name'))
|
||||||
|
reject_func()
|
||||||
|
return True
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="reject_wizard_view" model="ir.ui.view">
|
||||||
|
<field name="name">Reason Wizard</field>
|
||||||
|
<field name="model">all.reject.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Reason Wizard">
|
||||||
|
<group>
|
||||||
|
<field name="reason" required="1"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="button_confirm" string="Send" type="object" class="btn-primary"/>
|
||||||
|
<button special="cancel" string="Cancel" class="btn-default" />
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
Odoo Proprietary License v1.0
|
||||||
|
|
||||||
|
This software and associated files (the "Software") may only be used (executed,
|
||||||
|
modified, executed after modifications) if you have purchased a valid license
|
||||||
|
from the authors, typically via Odoo Apps, or if you have received a written
|
||||||
|
agreement from the authors of the Software (see the COPYRIGHT file).
|
||||||
|
|
||||||
|
You may develop Odoo modules that use the Software as a library (typically
|
||||||
|
by depending on it, importing it and using its resources), but without copying
|
||||||
|
any source code or material from the Software. You may distribute those
|
||||||
|
modules under the license of your choice, provided that this license is
|
||||||
|
compatible with the terms of the Odoo Proprietary License (For example:
|
||||||
|
LGPL, MIT, or proprietary licenses similar to this one).
|
||||||
|
|
||||||
|
It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||||
|
or modified copies of the Software.
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice must be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
|
from . import reports
|
||||||
|
from . import wizard
|
||||||
|
from .hooks import pre_init_hook, post_load, post_init_hook, uninstall_hook
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
"name": "KPI Balanced Scorecard",
|
||||||
|
"version": "18.0.1.0.4",
|
||||||
|
"category": "Extra Tools",
|
||||||
|
"author": "faOtools",
|
||||||
|
"website": "https://faotools.com/apps/18.0/kpi-balanced-scorecard-492",
|
||||||
|
"license": "Other proprietary",
|
||||||
|
"application": True,
|
||||||
|
"installable": True,
|
||||||
|
"auto_install": False,
|
||||||
|
"depends": [
|
||||||
|
"mail"
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
"security/security.xml",
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"data/data.xml",
|
||||||
|
"data/cron.xml",
|
||||||
|
"views/views.xml",
|
||||||
|
"wizard/kpi_copy_template.xml",
|
||||||
|
"views/kpi_measure_item.xml",
|
||||||
|
"views/kpi_measure.xml",
|
||||||
|
"views/kpi_constant.xml",
|
||||||
|
"views/kpi_item.xml",
|
||||||
|
"views/kpi_period.xml",
|
||||||
|
"views/kpi_category.xml",
|
||||||
|
"views/kpi_scorecard_line.xml",
|
||||||
|
"views/res_config_settings.xml",
|
||||||
|
"views/menu.xml",
|
||||||
|
"data/crm_measures.xml",
|
||||||
|
"data/sale_measures.xml",
|
||||||
|
"data/invoice_measures.xml",
|
||||||
|
"data/project_measures.xml"
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"web.assets_backend": [
|
||||||
|
"kpi_scorecard/static/lib/draggabilly/draggabilly.pkgd.js",
|
||||||
|
"kpi_scorecard/static/src/js/kpi_formula.js",
|
||||||
|
"kpi_scorecard/static/src/xml/kpi_formula_template.xml",
|
||||||
|
"kpi_scorecard/static/src/css/*.css",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"demo": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"external_dependencies": {},
|
||||||
|
"summary": "The tool to set up KPI targets and control their fulfillment by periods",
|
||||||
|
"description": """For the full details look at static/description/index.html
|
||||||
|
* Features *
|
||||||
|
- Real-time control and historical trends
|
||||||
|
- Drag-and-drop formulas for KPIs
|
||||||
|
- Shared KPIs and self-control
|
||||||
|
- KPI settings to process Odoo data
|
||||||
|
#odootools_proprietary""",
|
||||||
|
"images": [
|
||||||
|
"static/description/main.png"
|
||||||
|
],
|
||||||
|
"price": "198.0",
|
||||||
|
"currency": "EUR",
|
||||||
|
"live_test_url": "https://faotools.com/my/tickets/newticket?&url_app_id=138&ticket_version=14.0&url_type_id=3",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="crm_lead_all"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Leads and Opportunities: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_opportunities_only"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Opportunities: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="domain">[("type", "=", "opportunity")]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_leads_only"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Leads: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="domain">[("type", "=", "lead")]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_won_opprotunities"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Won Opportunities: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">date_closed</field>
|
||||||
|
<field name="domain">[("stage_id.is_won", "=", "True")]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_opportunities_days_to_assign"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Opportunities: Average Days to Assign</field>
|
||||||
|
<field name="measure_type">average</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">day_open</field>
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_opportunities_days_to_close"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Opportunities: Average Days to Close</field>
|
||||||
|
<field name="measure_type">average</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">day_close</field>
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_opportunities_expected_revenue"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Opportunities: Average Expected Revenue</field>
|
||||||
|
<field name="measure_type">average</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">expected_revenue</field>
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_opportunities_expected_revenue_total"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Opportunities: Total Expected Revenue</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">expected_revenue</field>
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_opportunities_sale_total"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Opportunities: Total Sale Orders</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">sale_amount_total</field>
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="crm_lead_opportunities_sale_average"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[crm] Opportunities: Average of Sale Orders</field>
|
||||||
|
<field name="measure_type">average</field>
|
||||||
|
<field name="model_name">crm.lead</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">sale_amount_total</field>
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="cron_recalculate_kpi_periods" model="ir.cron">
|
||||||
|
<field name="name">Calculate KPIs</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_period"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.action_cron_calculate_kpi()</field>
|
||||||
|
<field name="nextcall" eval="(DateTime.now() + timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')" />
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="interval_number">8</field>
|
||||||
|
<field name="interval_type">hours</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
|
||||||
|
<!-- <record id="mrp_workoorders_opened"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[TEST] TASKS</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">project.task</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">planned_hours</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record> -->
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<record id="customer_invoice_all"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[account] Posted Customer Invoices: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">account.move</field>
|
||||||
|
<field name="date_field_name">date</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">[("move_type", "=", "out_invoice"), ("state", "=", "posted")]</field>
|
||||||
|
</record>
|
||||||
|
<record id="vendor_invoice_all"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[account] Posted Vendor Bills: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">account.move</field>
|
||||||
|
<field name="date_field_name">date</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">[("move_type", "=", "in_invoice"), ("state", "=", "posted")]</field>
|
||||||
|
</record>
|
||||||
|
<record id="customer_invoice_amount_all"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[account] Posted Customer Invoices: Total</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">account.invoice.report</field>
|
||||||
|
<field name="date_field_name">invoice_date</field>
|
||||||
|
<field name="measure_field_name">price_subtotal</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">[("move_type", "=", "out_invoice"), ("state", "=", "posted")]</field>
|
||||||
|
</record>
|
||||||
|
<record id="vendor_bills_amount_all"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[account] Posted Customer Invoices: Total</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">account.invoice.report</field>
|
||||||
|
<field name="date_field_name">invoice_date</field>
|
||||||
|
<field name="measure_field_name">price_subtotal</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">[("move_type", "=", "in_invoice"), ("state", "=", "posted")]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<record id="project_task_all"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[project] Tasks: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">project.task</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="project_task_planned_hours"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[project] Tasks: Total Planned Hours</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">project.task</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">planned_hours</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="project_task_planned_hours_average"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[project] Tasks: Average Planned Hours</field>
|
||||||
|
<field name="measure_type">average</field>
|
||||||
|
<field name="model_name">project.task</field>
|
||||||
|
<field name="date_field_name">create_date</field>
|
||||||
|
<field name="measure_field_name">planned_hours</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="project_task_working_days_to_close"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[project] Tasks: Working Days to Close</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">report.project.task.user</field>
|
||||||
|
<field name="date_field_name">date_assign</field>
|
||||||
|
<field name="measure_field_name">working_days_close</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="project_task_working_days_to_assign"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[project] Tasks: Working Days to Assign</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">report.project.task.user</field>
|
||||||
|
<field name="date_field_name">date_assign</field>
|
||||||
|
<field name="measure_field_name">working_days_open</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="sale_order_all_abs"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[sales] All Sale Orders and Quotations: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">sale.order</field>
|
||||||
|
<field name="date_field_name">date_order</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
</record>
|
||||||
|
<record id="sale_order_all"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[sales] Сonfirmed/Done Sale Orders: Count</field>
|
||||||
|
<field name="measure_type">count</field>
|
||||||
|
<field name="model_name">sale.order</field>
|
||||||
|
<field name="date_field_name">date_order</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">["|", ("state","=","sale"), ("state","=","done")]</field>
|
||||||
|
</record>
|
||||||
|
<record id="sale_order_total"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[sales] Сonfirmed/Done Sale Orders: Total</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">sale.report</field>
|
||||||
|
<field name="date_field_name">date</field>
|
||||||
|
<field name="measure_field_name">price_total</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">["|", ("state","=","sale"), ("state","=","done")]</field>
|
||||||
|
</record>
|
||||||
|
<record id="sale_order_total_qty"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[sales]Сonfirmed/Done Sale Orders: Product Units</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">sale.report</field>
|
||||||
|
<field name="date_field_name">date</field>
|
||||||
|
<field name="measure_field_name">product_uom_qty</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">["|", ("state","=","sale"), ("state","=","done")]</field>
|
||||||
|
</record>
|
||||||
|
<record id="sale_order_invoiced_qty"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[sales] Сonfirmed/Done Sale Orders: Invoiced Units</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">sale.report</field>
|
||||||
|
<field name="date_field_name">date</field>
|
||||||
|
<field name="measure_field_name">qty_invoiced</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">["|", ("state","=","sale"), ("state","=","done")]</field>
|
||||||
|
</record>
|
||||||
|
<record id="sale_order_delivered_qty"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[sales] Сonfirmed/Done Sale Orders: Delivered Units</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">sale.report</field>
|
||||||
|
<field name="date_field_name">date</field>
|
||||||
|
<field name="measure_field_name">qty_delivered</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">["|", ("state","=","sale"), ("state","=","done")]</field>
|
||||||
|
</record>
|
||||||
|
<record id="sale_order_number_of_lines"
|
||||||
|
forcecreate="True"
|
||||||
|
model="kpi.measure"
|
||||||
|
>
|
||||||
|
<field name="name">[sales] Сonfirmed/Done Sale Orders: Number of Lines</field>
|
||||||
|
<field name="measure_type">sum</field>
|
||||||
|
<field name="model_name">sale.report</field>
|
||||||
|
<field name="date_field_name">date</field>
|
||||||
|
<field name="measure_field_name">nbr</field>
|
||||||
|
<field name="company_field_name">company_id</field>
|
||||||
|
<field name="domain">["|", ("state","=","sale"), ("state","=","done")]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
def pre_init_hook(cr):
|
||||||
|
"""Loaded before installing the module.
|
||||||
|
|
||||||
|
None of this module's DB modifications will be available yet.
|
||||||
|
|
||||||
|
If you plan to raise an exception to abort install, put all code inside a
|
||||||
|
``with cr.savepoint():`` block to avoid broken databases.
|
||||||
|
|
||||||
|
:param openerp.sql_db.Cursor cr:
|
||||||
|
Database cursor.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def post_init_hook(cr, registry):
|
||||||
|
"""Loaded after installing the module.
|
||||||
|
|
||||||
|
This module's DB modifications will be available.
|
||||||
|
|
||||||
|
:param openerp.sql_db.Cursor cr:
|
||||||
|
Database cursor.
|
||||||
|
|
||||||
|
:param openerp.modules.registry.RegistryManager registry:
|
||||||
|
Database registry, using v7 api.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall_hook(cr, registry):
|
||||||
|
"""Loaded before uninstalling the module.
|
||||||
|
|
||||||
|
This module's DB modifications will still be available. Raise an exception
|
||||||
|
to abort uninstallation.
|
||||||
|
|
||||||
|
:param openerp.sql_db.Cursor cr:
|
||||||
|
Database cursor.
|
||||||
|
|
||||||
|
:param openerp.modules.registry.RegistryManager registry:
|
||||||
|
Database registry, using v7 api.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def post_load():
|
||||||
|
"""Loaded before any model or data has been initialized.
|
||||||
|
|
||||||
|
This is ok as the post-load hook is for server-wide
|
||||||
|
(instead of registry-specific) functionalities.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import kpi_help
|
||||||
|
from . import kpi_period
|
||||||
|
from . import kpi_constant
|
||||||
|
from . import kpi_period_value
|
||||||
|
from . import kpi_measure
|
||||||
|
from . import kpi_measure_item
|
||||||
|
from . import kpi_item
|
||||||
|
from . import kpi_scorecard_line
|
||||||
|
from . import kpi_category
|
||||||
|
from . import res_company
|
||||||
|
from . import res_config_settings
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class kpi_category(models.Model):
|
||||||
|
"""
|
||||||
|
The model to structure KPIs and targets
|
||||||
|
"""
|
||||||
|
_name = "kpi.category"
|
||||||
|
_inherit = "kpi.help"
|
||||||
|
_description = "KPI Category"
|
||||||
|
|
||||||
|
@api.depends("user_ids", "user_group_ids", "user_group_ids.users")
|
||||||
|
def _compute_access_user_ids(self):
|
||||||
|
"""
|
||||||
|
Compute method for access_user_ids
|
||||||
|
"""
|
||||||
|
for category in self:
|
||||||
|
users = category.user_group_ids.mapped("users") + category.user_ids
|
||||||
|
category.access_user_ids = [(6, 0, users.ids)]
|
||||||
|
|
||||||
|
@api.depends("edit_user_ids", "edit_user_group_ids", "edit_user_group_ids.users")
|
||||||
|
def _compute_edit_access_user_ids(self):
|
||||||
|
"""
|
||||||
|
Compute method for edit_access_user_ids
|
||||||
|
"""
|
||||||
|
for category in self:
|
||||||
|
users = category.edit_user_group_ids.mapped("users") + category.edit_user_ids
|
||||||
|
category.edit_access_user_ids = [(6, 0, users.ids)]
|
||||||
|
|
||||||
|
@api.constrains('parent_id')
|
||||||
|
def _check_node_recursion(self):
|
||||||
|
"""
|
||||||
|
Constraint for recursion
|
||||||
|
"""
|
||||||
|
if not self._check_recursion():
|
||||||
|
raise ValidationError(_('It is not allowed to make recursions!'))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _inverse_active(self):
|
||||||
|
"""
|
||||||
|
Inverse method for active to deactivate all child categories
|
||||||
|
"""
|
||||||
|
for category in self:
|
||||||
|
if not category.active:
|
||||||
|
for categ in category.child_ids:
|
||||||
|
categ.active = False
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string="Name",
|
||||||
|
required=True,
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
active = fields.Boolean(
|
||||||
|
string="Active",
|
||||||
|
default=True,
|
||||||
|
inverse=_inverse_active,
|
||||||
|
)
|
||||||
|
parent_id = fields.Many2one(
|
||||||
|
"kpi.category",
|
||||||
|
string="Parent Category",
|
||||||
|
)
|
||||||
|
child_ids = fields.One2many(
|
||||||
|
"kpi.category",
|
||||||
|
"parent_id",
|
||||||
|
string="Child Categories",
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
string="Company",
|
||||||
|
)
|
||||||
|
description = fields.Text(
|
||||||
|
string="Notes",
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
sequence = fields.Integer(string="Sequence")
|
||||||
|
user_ids = fields.Many2many(
|
||||||
|
"res.users",
|
||||||
|
"res_users_kpi_category_rel_table",
|
||||||
|
"res_user_rel_id",
|
||||||
|
"kpi_category_id",
|
||||||
|
string="Read Rights Users",
|
||||||
|
)
|
||||||
|
user_group_ids = fields.Many2many(
|
||||||
|
"res.groups",
|
||||||
|
"res_groups_kkpi_category_rel_table",
|
||||||
|
"res_groups_id",
|
||||||
|
"kpi_category_id",
|
||||||
|
string="Read Rights User Groups",
|
||||||
|
)
|
||||||
|
access_user_ids = fields.Many2many(
|
||||||
|
"res.users",
|
||||||
|
"res_users_kpi_category_all_rel_table",
|
||||||
|
"res_user_all_rel_id",
|
||||||
|
"kpi_category_all_id",
|
||||||
|
string="Read Rights Access Users",
|
||||||
|
compute=_compute_access_user_ids,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
edit_user_ids = fields.Many2many(
|
||||||
|
"res.users",
|
||||||
|
"res_users_kpi_category_edit_rel_table",
|
||||||
|
"res_user_rel_id",
|
||||||
|
"kpi_category_id",
|
||||||
|
string="Edit Rights Allowed Users",
|
||||||
|
)
|
||||||
|
edit_user_group_ids = fields.Many2many(
|
||||||
|
"res.groups",
|
||||||
|
"res_groups_kpi_category_edit_rel_table",
|
||||||
|
"res_groups_id",
|
||||||
|
"kpi_category_id",
|
||||||
|
string="Edit Rights User Groups",
|
||||||
|
)
|
||||||
|
edit_access_user_ids = fields.Many2many(
|
||||||
|
"res.users",
|
||||||
|
"res_users_kpi_category_edit_all_rel_table",
|
||||||
|
"res_user_all_rel_id",
|
||||||
|
"kpi_category_all_id",
|
||||||
|
string="Edit Rights Access Users",
|
||||||
|
compute=_compute_edit_access_user_ids,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_order = "sequence, id"
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
"""
|
||||||
|
Overloading the method, to reflect parent's name recursively
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for node in self:
|
||||||
|
name = u"{}{}".format(
|
||||||
|
node.parent_id and node.parent_id.name_get()[0][1] + '/' or '',
|
||||||
|
node.name,
|
||||||
|
)
|
||||||
|
result.append((node.id, name))
|
||||||
|
return result
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_return_nodes(self):
|
||||||
|
"""
|
||||||
|
The method to return nodes in jstree format
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _return_nodes_recursive
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* list of folders dict with keys:
|
||||||
|
** id
|
||||||
|
** text - folder_name
|
||||||
|
** icon
|
||||||
|
** children - array with the same keys
|
||||||
|
"""
|
||||||
|
self = self.with_context(lang=self.env.user.lang)
|
||||||
|
nodes = self.search([("parent_id", "=", False)])
|
||||||
|
res = []
|
||||||
|
for node in nodes:
|
||||||
|
res.append(node._return_nodes_recursive())
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _return_nodes_recursive(self):
|
||||||
|
"""
|
||||||
|
The method to go by all nodes recursively to prepare their list in js_tree format
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* sorted needed to fix unclear bug of zero-sequence element placed to the end
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
res = {
|
||||||
|
"text": self.name,
|
||||||
|
"id": self.id,
|
||||||
|
}
|
||||||
|
if self._context.get("show_tooltip") and hasattr(self, "description") and self.description not in EMPTYHTML:
|
||||||
|
res.update({"a_attr": {"kn_tip": self.description},})
|
||||||
|
child_res = []
|
||||||
|
for child in self.child_ids.sorted(lambda ch: ch.sequence):
|
||||||
|
child_res.append(child._return_nodes_recursive())
|
||||||
|
res.update({"children": child_res})
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class kpi_constant(models.Model):
|
||||||
|
"""
|
||||||
|
The model to use static figures for calculations
|
||||||
|
"""
|
||||||
|
_name = "kpi.constant"
|
||||||
|
_inherit = "kpi.help"
|
||||||
|
_description = "KPI Constant"
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string="Name",
|
||||||
|
required=True,
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
target_value = fields.Float(
|
||||||
|
"Global Value",
|
||||||
|
help="Value used in case this constant is not applied for a target period",
|
||||||
|
)
|
||||||
|
periods_ids = fields.One2many(
|
||||||
|
"kpi.period.value",
|
||||||
|
"constant_id",
|
||||||
|
string="Set for periods",
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
string="Company",
|
||||||
|
)
|
||||||
|
active = fields.Boolean(
|
||||||
|
string="Active",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
description = fields.Text(
|
||||||
|
string="Notes",
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
sequence = fields.Integer(string="Sequence")
|
||||||
|
|
||||||
|
_order = "sequence, id"
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
"""
|
||||||
|
The method to block unlink if used in any KPI
|
||||||
|
"""
|
||||||
|
for constant in self:
|
||||||
|
const_key = "CONST({})".format(constant.id)
|
||||||
|
domain = [
|
||||||
|
("formula", "like", const_key),
|
||||||
|
"|",
|
||||||
|
("active", "=", False),
|
||||||
|
("active", "=", True),
|
||||||
|
]
|
||||||
|
kpi_id = self.env["kpi.item"].sudo().search(domain, limit=1)
|
||||||
|
if kpi_id:
|
||||||
|
raise ValidationError(
|
||||||
|
_("There are KPIs which depend on this constant: {}. Delete them before".format(kpi_id))
|
||||||
|
)
|
||||||
|
super(kpi_constant, self).unlink()
|
||||||
|
|
||||||
|
def _get_value_by_period(self, period_id):
|
||||||
|
"""
|
||||||
|
The method to find target value by date
|
||||||
|
We search the most preceis constant definition: if it monthly period > but there is no constant for this month,
|
||||||
|
we also check quarterly and yearly intervals
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* period_id - kpi.period
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* float
|
||||||
|
|
||||||
|
Extra info
|
||||||
|
* the method is hierarchicallyr recursive to check parents
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
target_value = self.target_value
|
||||||
|
for constant_period in self.periods_ids:
|
||||||
|
if period_id == constant_period.period_id:
|
||||||
|
target_value = constant_period.target_value
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if period_id.parent_id:
|
||||||
|
target_value = self._get_value_by_period(period_id.parent_id)
|
||||||
|
else:
|
||||||
|
target_value = self.target_value
|
||||||
|
return target_value
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
|
||||||
|
help_dict = {
|
||||||
|
"kpi.period": _("""
|
||||||
|
<div style="width:80%;font-size:14px;">
|
||||||
|
<p style="padding-bottom:10px;">A KPI period is a time frame for which companies set their targets to control
|
||||||
|
performance (in a similar way as financial performance is controlled within accounting/fiscal periods).</p>
|
||||||
|
<p style="padding-bottom:10px;">KPI scorecards in comparison to simple dashboard assume that you might not only have
|
||||||
|
overview of actual figures, but to compare those to real targets. However, goals should be time-constrained ('Sell as
|
||||||
|
much as possible' is not a goal, while 'Generate 100,000 Euro Revenue in the year 2021' is a good target for a sales
|
||||||
|
person, for example). That is why KPI periods are introduced.</p>
|
||||||
|
<p style="padding-bottom:10px;">The app allows to set periods of any length including intervals which cross each other
|
||||||
|
(and even the Past periods!).
|
||||||
|
To open a new KPI period you should just set start and end dates. Those dates will be used to calculate KPI actual value.
|
||||||
|
For example, KPI formula might include a basic KPI measurement for total amount of sale orders, while date field of
|
||||||
|
this measurement is defined as 'Order date'. Then, only sale orders which order date is within the period would be
|
||||||
|
taken into account. Thus, KPI scorecard would show total revenue for a given period.</p>
|
||||||
|
<p style="padding-bottom:10px;">It is recommended to have more or less strict logic of periods for comparability
|
||||||
|
purposes. Usually, KPI targets are set for a whole year and quarterly/monthly intervals. It let not only have KPI
|
||||||
|
overview, but also check historical trends. The app automatically considers various periods in order to define which
|
||||||
|
might be compared to this one, and will show users a chart of actual values by a specific KPI. It is possible to define
|
||||||
|
tolerance on the configuration page. For example, 2-days tolerance is needed to compare quarterly periods (since quarter
|
||||||
|
might take from 90 to 92 days), and 3-days tolerance for months (unluckily, February may last 28 days).</p>
|
||||||
|
<p style="padding-bottom:10px;">When end date is already in the Past, it is preferable to close the period to avoid
|
||||||
|
further updates of KPI targets actual values due to further corrections (it is not a good idea to update December KPIs
|
||||||
|
in the next August even though a sale total needs to be corrected, for example). This action is pretty much the same
|
||||||
|
as when accountant finishes a fiscal year. To close a period use the button 'Close period' (this action might be rolled
|
||||||
|
back by re-opening the period).</p>
|
||||||
|
<p style="padding-bottom:10px;">KPI periods let you also copy targets from existing periods (so, use them as a
|
||||||
|
template). To that end just push the button 'Substitute targets' and select a period with proper KPI targets.
|
||||||
|
It significantly saves time, since KPI targets usually remain similar for periods of the same length. Do not worry,
|
||||||
|
after that action you would be still able to change actual scorecard for this period (for sure, until a period is
|
||||||
|
closed).</p>
|
||||||
|
<p style="padding-bottom:10px;">Based on KPI periods you may also configure KPI formulas to rely upon period length in
|
||||||
|
days to calculate figures per days, weeks, etc. To that end use the special Measurements 'Periods Days' or 'Days Passed'
|
||||||
|
(how many days already passed in comparison to today) when you construct a formula. For example, you might have average
|
||||||
|
sales amount per week within the given period, and compare how much it changes from the previous one. That length is
|
||||||
|
calculated automatically, while you just put that to a proper formula place.</p>
|
||||||
|
</div>
|
||||||
|
"""),
|
||||||
|
"kpi.item": _("""
|
||||||
|
<div style="width:80%;font-size:14px;">
|
||||||
|
<p style="padding-bottom:10px;">KPIs (key performance indicators) are measurements which aim to evaluate organizational
|
||||||
|
or personal success of a definite activity. Different companies have different KPIs depending on their strategy and
|
||||||
|
business area. However, all KPIs have the common core attribute: they must be measurable within a target period.</p>
|
||||||
|
<p style="padding-bottom:10px;">To that end KPIs allow to construct formulas to retrieve Odoo data sets and process
|
||||||
|
those to real figures. Formula preparation is as simple as it is to write down a mathematical expression: just drag and
|
||||||
|
drop formula parts in a right order with correct operators.</p>
|
||||||
|
<h3>KPIs, KPI periods, and KPI targets</h3>
|
||||||
|
<p style="padding-bottom:10px;">KPIs represent the list of success figures you may use to plan your company activities.
|
||||||
|
Simultaneously, measurements are almost senseless unless you have target values for those KPIs. That is possible only
|
||||||
|
within a time-constrained period. 'Sell as much as you can' is not a goal, while 'Generate 100,000 Euro Revenue in the
|
||||||
|
year 2021' is a good aim for a sales person, for example. To that end KPI periods are introduced.</p>
|
||||||
|
<p style="padding-bottom:10px;">A Combination of a KPI and a KPI period results in a KPI target. Exactly with those
|
||||||
|
targets you work on the score card interface. For each KPI you would like to manage in this period, you should define
|
||||||
|
a planned value to compare those to actual at the end of the period.</p>
|
||||||
|
<p style="padding-bottom:10px;">So, a KPI itself defines how to compute actual value for period and how to estimate the
|
||||||
|
result ('the more the better' or 'the less the better'), but does not assume setting targets. The latter should be done
|
||||||
|
for each period.</p>
|
||||||
|
<h3>Formula parts</h3>
|
||||||
|
<p style="padding-bottom:10px;">As variables for formula you may use:</p>
|
||||||
|
<p style="padding-bottom:10px;"><i>Measurements (KPI measurements, KPI variables)</i> are figures calculated from actual
|
||||||
|
Odoo data for a checked period. For example, 'number of quotations of the sales team Europe'.</p>
|
||||||
|
<p style="padding-bottom:10px;">Among measurements you may also find 2 very specific variables: 'Period Days' and 'Days
|
||||||
|
Passed'. Those figures are calculated not from Odoo data, but from the KPI period settings under consideration. 'Period
|
||||||
|
Days' is an interval length in days. 'Days Passed' is a length between period start and today (if today is before period
|
||||||
|
end; otherwise period end). Those parameters let calculate per-time KPIs, such as, for example, 'Average sales per
|
||||||
|
week'.</p>
|
||||||
|
<p style="padding-bottom:10px;"><i>Constants (KPI constants)</i> are fixed numbers applied globally or for a period.
|
||||||
|
Such numbers do not depend on Odoo data. So, they let define strict non-changeable figures which you can't otherwise get
|
||||||
|
from Odoo. For example, 'Total investments'.</p>
|
||||||
|
<p style="padding-bottom:10px;"><i>Other KPIs</i> are results of other KPI formula calculations. That variables allow to
|
||||||
|
construct derivative complex calculations and make up hierarchy. For example, you may have KPIs 'Sales count' and
|
||||||
|
'Opportunities count', and a derivative KPI 'Opportunity to sales success ratio'.</p>
|
||||||
|
<p style="padding-bottom:10px;">Variables of any types above might be added to a formula. You may drag and drop as many
|
||||||
|
variables as you like (and even use the same variable twice). Just do not forget to add operators in between to make
|
||||||
|
correct mathematical expressions. The following operators are available:</p>
|
||||||
|
<ul>
|
||||||
|
<li>"-" - subtraction;</li>
|
||||||
|
<li>"+" - addition;</li>
|
||||||
|
<li>"*" - multiplication;</li>
|
||||||
|
<li>"/" - division;</li>
|
||||||
|
<li>"(", ")" - to make proper calculation order as it is in Math;</li>
|
||||||
|
<li>"**" - exponentiation (**2 – squaring; **0.5 – square root extraction);</li>
|
||||||
|
<li>Float number.</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Result appearance</h3>
|
||||||
|
<p style="padding-bottom:10px;">Depending of business logic of KPI formula, the final calculation result might have
|
||||||
|
different form:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Simple number: for example, 'Average sales count per week';</li>
|
||||||
|
<li>Percentage: for example, 'Sales to opportunities success ratio';</li>
|
||||||
|
<li>Monetary: for example, 'Total Sales per period'. <strong>Make sure the measurement field you used is in the same
|
||||||
|
currency (usually company default currency)!</strong></li>
|
||||||
|
</ul>
|
||||||
|
<p style="padding-bottom:10px;">For simple numbers and percentage you may also define result suffix and prefix to make a
|
||||||
|
figure nice looking (e.g. add "%" as suffix to have "88<strong>%</strong>"). For monetary result type it is recommended
|
||||||
|
to define a currency, which symbol would be added to result.</p>
|
||||||
|
<p style="padding-bottom:10px;">Finally, you may decide how calculation result should be rounded. Available options are
|
||||||
|
from 0 to 4 decimal points (1 > 1.2 > 1.23 > 1.235 > 1.2346).</p>
|
||||||
|
<h3>Categories and hierarchy</h3>
|
||||||
|
<p style="padding-bottom:10px;">To make navigation by KPI targets more comfortable, KPIs are combined into KPI
|
||||||
|
categories. It let not only quickly search targets inside a scorecard, but it let grant additive access rights. In such
|
||||||
|
a way, users would overview only their KPIs structured in sections.</p>
|
||||||
|
<p style="padding-bottom:10px;">Each KPI might also have a parent. Such hierarchy let organize KPI scorecard with
|
||||||
|
indicative padding. For example, 'Total company sales' might have children 'Sales Europe' and 'Sales America'. The
|
||||||
|
latter 2 might be further specified by sales persons.</p>
|
||||||
|
<h3>Additive access to KPIs and targets</h3>
|
||||||
|
<p style="padding-bottom:10px;">By default KPIs and there linked targets are available only for users with the right
|
||||||
|
'KPI Manager'. Simultaneously, you may grant extra rights for other user groups or/and definite users. To that end it is
|
||||||
|
possible to define 'Read Rights' and 'Edit Rights' on KPI category or KPI form views (take into account that KPI
|
||||||
|
category rights and KPI own rights are combined!).</p>
|
||||||
|
<p style="padding-bottom:10px;">Read rights define which user and user groups would be able to observe KPI targets on
|
||||||
|
the scorecard interface. For example, you might want to share tasks' targets by persons with related project users in
|
||||||
|
order their control themselves.</p>
|
||||||
|
<p style="padding-bottom:10px;">Edit rights assume sharing an access to set specific targets up. For instance, you may
|
||||||
|
find it a good idea to involve sales manager to set sub targets for their sales team.</p>
|
||||||
|
<p style="padding-bottom:10px;">Take into account: all security settings are additive and they are not restrictive. KPI
|
||||||
|
managers would have full rights for all KPIs disregarding those settings, while other users would have rights only to
|
||||||
|
KPIs which settings (or category settings) allow them so.</p>
|
||||||
|
</div>
|
||||||
|
"""),
|
||||||
|
"kpi.measure.item": _("""
|
||||||
|
<div style="width:80%;">
|
||||||
|
<p style="padding-bottom:10px;">A KPI measurement is a final variable used for KPI(s) calculations. It represents
|
||||||
|
specification of basic measurement. For example, 'number of quotations of the sales team Europe' might be a precision of
|
||||||
|
a basic measurement 'total number of sales orders'.</p>
|
||||||
|
<p style="padding-bottom:10px;">Such approach significantly simplifies variables' preparation, since each basic
|
||||||
|
measurement might have an unlimited number of linked KPI measurements. So, the only thing you would need to do is to
|
||||||
|
apply extra filters (in the example 'Sales Team Name is Europe').</p>
|
||||||
|
<p style="padding-bottom:10px;">Take into account that basic measurements of the type 'Execute Python code' can't be
|
||||||
|
any more specified, since they do not relate to any records. In such a case, there is no sense to have a few KPI
|
||||||
|
measurements.</p>
|
||||||
|
<p style="padding-bottom:10px;">In a multi company environment KPI Measurements are applied globally or for each company
|
||||||
|
individually. In the former case that variable is available for any company KPI formulas, while in the latter – only for
|
||||||
|
specific one (it let make quicker overview while constructing formulas).</p>
|
||||||
|
</div>
|
||||||
|
"""),
|
||||||
|
"kpi.constant": _("""
|
||||||
|
<div style="width:80%;">
|
||||||
|
<p style="padding-bottom:10px;">
|
||||||
|
A KPI constant is a variable type used for formula construction. In comparison to measurements, KPI constants are fixed
|
||||||
|
and they do not depend on actual Odoo data. This allows to introduce figures which can not be retrieved from modules
|
||||||
|
and/or which should remain the same during the whole period.</p>
|
||||||
|
<p style="padding-bottom:10px;">For example, you might set the 'total size of investments' to calculate return on
|
||||||
|
investments, or the 'number of salesmen' to get sales revenue per person.</p>
|
||||||
|
<p style="padding-bottom:10px;">KPI constant value might be defined for each individual period. If the value does not
|
||||||
|
exist for a calculated period, the app would try to check the parent time frame. For instance, if this constant value
|
||||||
|
does is not set up January 2021, the app would take all the intervals which include January (e.g. Quarter 1 2021 and the
|
||||||
|
year 2021 consequentially). In case the values is not defined for those periods as well, then, the global value would be
|
||||||
|
applied.</p>
|
||||||
|
</div>
|
||||||
|
"""),
|
||||||
|
"kpi.measure": _("""
|
||||||
|
<div style="width:80%;font-size:14px;">
|
||||||
|
<p style="padding-bottom:10px;">A basic measurement is the core object used for retrieving actual KPI value from Odoo
|
||||||
|
data. Although basic measurements are not used themselves for formula constructions, they are required to prepare any
|
||||||
|
sort of formula variables (KPI measurements).</p>
|
||||||
|
<p style="padding-bottom:10px;">A basic measurement represents the most general calculation, while KPI measurements
|
||||||
|
specify those. For example, 'total number of sales orders' should be a basic measurement, while narrower 'number of
|
||||||
|
quotations of the sales team Europe' is recommended to be a precision of that basic measurement (so, KPI measurement).
|
||||||
|
Each basic measurement might have an unlimited number of linked KPI measurements.</p>
|
||||||
|
<h3>Calculation Types</h3>
|
||||||
|
<p style="padding-bottom:10px;">Basic measurements assume a few types of low-level calculations:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Counting records, e.g. number of registered leads or a number of posted customer invoices. Such measurements should
|
||||||
|
have the KPI type 'Count of records'.</li>
|
||||||
|
<li>Summing up certain number field of records, e.g. sum of all orders amount total or sum of paid taxes by invoices.
|
||||||
|
Such measurements should have the KPI type 'Sum of records field'.</li>
|
||||||
|
<li>Getting of average for records' number fields, e.g. average planned hours per task or average days to assign a lead.
|
||||||
|
Such measurements should have the KPI type 'Average of records field'.</li>
|
||||||
|
<li>Executing Python code. This type requires technical knowledge, but let you compute any sort of figures based on any
|
||||||
|
Odoo data. Merely introduce your Python code and save the value into the special variable 'result'. Here you might also
|
||||||
|
use the special KPI period related variables: 'period_start' (the first date of the period), 'period_end' (the last date
|
||||||
|
of the period), 'period_company_id' - res.company object for which Odoo makes calculations at the moment (according to
|
||||||
|
KPI period).</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Basic Measurement Settings</h3>
|
||||||
|
<p style="padding-bottom:10px;">The first 3 calculation types assume that you define how records should be searched and
|
||||||
|
which records fields should be used for computations.</p>
|
||||||
|
<ul>
|
||||||
|
<li><i>Model</i> is an Odoo document type you have in your database, so with which data set you work. For, examples,
|
||||||
|
'Sales Order' or 'Task'. Here you can rely not only upon standard objects, but also on Odoo reports. The latter is quite
|
||||||
|
useful if indicators are already calculated for existing dashboard (for example, total sales amount in default currency
|
||||||
|
from the 'Sales Analysis Report').</li>
|
||||||
|
<li><i>Date fields</i> are required to understand whether a specific document type relates to a considered period, so,
|
||||||
|
how to distribute objects by time intervals. For example, for tasks you might use 'create date' to analyze jobs
|
||||||
|
registered within this KPI period (e.g. 'Total number of tasks created in January 2021'). It is possible to apply a few
|
||||||
|
date fields (e.g. 'Opportunities opened and won in January 2021'). If date fields are not specified, KPI period would
|
||||||
|
not influence this basic measurement.</li>
|
||||||
|
<li><i>Filters</i> allow you to restrict records set by any stored field. For example, you may calculate count of only
|
||||||
|
won opportunities based on stage settings or only posted customer invoices based on journal entry type and state.</li>
|
||||||
|
<li><i>Measure field</i> is available and required only for calculation types 'Average' and 'Sum'. It defines which
|
||||||
|
figure you use for calculations. For example, total amount of Sales Analysis Report to get accumulated sales revenue or
|
||||||
|
work hours of tasks to get average spent time per each task.</li>
|
||||||
|
<li><i>Company field</i> would be needed for multi companies environment. According to that field, KPIs are considered
|
||||||
|
only withing a KPI period target company. Take into account that records without company stated would be used for
|
||||||
|
all companies' KPI calculations.</li>
|
||||||
|
</ul>
|
||||||
|
<p style="padding-bottom:10px;">All settings might relate to your custom objects or custom fields, including ones
|
||||||
|
created through the interface or the Odoo studio.</p>
|
||||||
|
</div>
|
||||||
|
"""),
|
||||||
|
"kpi.category": _("""
|
||||||
|
<div style="width:60%;">
|
||||||
|
<p style="padding-bottom:10px;">KPI categories serve to structure KPIs and KPI targets for comfortable navigation. Each
|
||||||
|
KPI should be assigned for a single category, what allows users to find required targets quickly just by checking the
|
||||||
|
boxes on the scorecard interface.</p>
|
||||||
|
<p style="padding-bottom:10px;">Hierarchy of categories let users also combine targets in sections to control KPIs
|
||||||
|
related to specific areas. For example, to check targets only in sales (e.g. category 'sales') or targets of a
|
||||||
|
specific sales team (e.g. category 'sales > sales team Europe').</p>
|
||||||
|
<p style="padding-bottom:10px;">Moreover, KPI categories let you administrate user accesses in a batch. Thus, you may
|
||||||
|
grant users and/or user groups an access for this category KPI ('Read Rights') or a right to update those
|
||||||
|
targets ('Edit rights)'. Thus, there would be no need to manage each KPI separately. Take into account that those
|
||||||
|
settings are additive and are not restrictive, meaning that KPI managers would any way have full rights for all
|
||||||
|
KPIs.</p>
|
||||||
|
</div>
|
||||||
|
"""),
|
||||||
|
"kpi.scorecard.line": _("""
|
||||||
|
<p style="padding-bottom:10px;"></p>A KPI target is your plan for this KPI for a given period. By setting up a target
|
||||||
|
value, you indicate which result you would like to achieve by the end of the period.</p>
|
||||||
|
<p style="padding-bottom:10px;"></p>KPI targets actual values are re-calculated regularly (not in real time) and
|
||||||
|
automatically by the Odoo cron job. Alternatively, you may press the button 'Calculate' on the left navigation bar.</p>
|
||||||
|
"""),
|
||||||
|
"kpi.copy.template": _("""
|
||||||
|
<p style="padding-bottom:10px;">The action removes all current targets and copies targets from a chosen period. After
|
||||||
|
that action you would be still able to modify scorecard: change targets' values, delete certain KPI targets, or add new
|
||||||
|
ones.</p>
|
||||||
|
"""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class kpi_help(models.AbstractModel):
|
||||||
|
"""
|
||||||
|
The model to store help settings
|
||||||
|
"""
|
||||||
|
_name = "kpi.help"
|
||||||
|
_description = "KPI Help"
|
||||||
|
|
||||||
|
@api.depends("kpi_help_dummy")
|
||||||
|
def _compute_show_kpi_help(self):
|
||||||
|
"""
|
||||||
|
Compute method for show_kpi_help
|
||||||
|
"""
|
||||||
|
help_setting = self.env.user.company_id.show_kpi_help
|
||||||
|
help_notes = help_setting and help_dict.get(self._name) or False
|
||||||
|
for record in self:
|
||||||
|
record.help_notes = help_notes
|
||||||
|
|
||||||
|
help_notes = fields.Html(
|
||||||
|
string="Help",
|
||||||
|
compute=_compute_show_kpi_help,
|
||||||
|
)
|
||||||
|
kpi_help_dummy = fields.Boolean("Dummy Help")
|
||||||
|
|
||||||
|
|
@ -0,0 +1,670 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tools.safe_eval import safe_eval
|
||||||
|
|
||||||
|
|
||||||
|
OPERANDS = ["-", "+", "*", "/", "(", ")", "**"]
|
||||||
|
ERRORFORM = _("<ERROR!!!FORMULA PART DOES NOT EXIST>")
|
||||||
|
|
||||||
|
def cut_name_part(part):
|
||||||
|
"""
|
||||||
|
The method to cut part name
|
||||||
|
"""
|
||||||
|
if part and len(part) > 40:
|
||||||
|
part = part[:38] + ".."
|
||||||
|
return part
|
||||||
|
|
||||||
|
class kpi_item(models.Model):
|
||||||
|
"""
|
||||||
|
The key model for KPI calulation
|
||||||
|
"""
|
||||||
|
_name = "kpi.item"
|
||||||
|
_inherit = "kpi.help"
|
||||||
|
_description = "KPI"
|
||||||
|
|
||||||
|
@api.depends("formula")
|
||||||
|
def _compute_measures_ids(self):
|
||||||
|
"""
|
||||||
|
Compute method for measures_ids, constant_ids, kpi_ids, formula_warning
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* action_render_formula
|
||||||
|
* _check_formula_with_testing_data
|
||||||
|
"""
|
||||||
|
self = self.sudo()
|
||||||
|
for kpi in self:
|
||||||
|
formula_parts = kpi.action_render_formula(kpi.formula)
|
||||||
|
measures_ids = []
|
||||||
|
constant_ids = []
|
||||||
|
kpi_ids = []
|
||||||
|
for part in formula_parts:
|
||||||
|
if part.get("type") == "MEASURE":
|
||||||
|
measures_ids.append(part.get("res_id"))
|
||||||
|
elif part.get("type") == "CONST":
|
||||||
|
constant_ids.append(part.get("res_id"))
|
||||||
|
elif part.get("type") == "KPI":
|
||||||
|
kpi_ids.append(part.get("res_id"))
|
||||||
|
measures_ids = self.env["kpi.measure.item"].browse(measures_ids).exists().ids
|
||||||
|
kpi.measures_ids = [(6, 0, measures_ids)]
|
||||||
|
constant_ids = self.env["kpi.constant"].browse(constant_ids).exists().ids
|
||||||
|
kpi.constant_ids = [(6, 0, constant_ids)]
|
||||||
|
kpi_ids = self.env["kpi.item"].browse(kpi_ids).exists().ids
|
||||||
|
kpi.kpi_ids = [(6, 0, kpi_ids)]
|
||||||
|
kpi.formula_warning = kpi._check_formula_with_testing_data()
|
||||||
|
|
||||||
|
@api.depends("parent_id", "parent_id.all_parent_ids")
|
||||||
|
def _compute_all_parent_ids(self):
|
||||||
|
"""
|
||||||
|
Compute method for all_parent_ids. The idea is have full hierarchy for dependance
|
||||||
|
"""
|
||||||
|
for kpi in self:
|
||||||
|
kpi.all_parent_ids = [(6, 0, (kpi.parent_id + kpi.parent_id.all_parent_ids).ids)]
|
||||||
|
|
||||||
|
@api.depends("user_ids", "user_group_ids", "user_group_ids.users", "category_id", "category_id.access_user_ids")
|
||||||
|
def _compute_access_user_ids(self):
|
||||||
|
"""
|
||||||
|
Compute method for access_user_ids
|
||||||
|
"""
|
||||||
|
for line in self:
|
||||||
|
own_users = line.user_group_ids.mapped("users") + line.user_ids
|
||||||
|
category_users = line.category_id.access_user_ids
|
||||||
|
users = own_users + category_users
|
||||||
|
line.access_user_ids = [(6, 0, users.ids)]
|
||||||
|
|
||||||
|
@api.depends("edit_user_ids", "edit_user_group_ids", "edit_user_group_ids.users", "category_id",
|
||||||
|
"category_id.edit_access_user_ids")
|
||||||
|
def _compute_edit_access_user_ids(self):
|
||||||
|
"""
|
||||||
|
Compute method for edit_access_user_ids
|
||||||
|
"""
|
||||||
|
for line in self:
|
||||||
|
own_users = line.edit_user_group_ids.mapped("users") + line.edit_user_ids
|
||||||
|
category_users = line.category_id.edit_access_user_ids
|
||||||
|
users = own_users + category_users
|
||||||
|
line.edit_access_user_ids = [(6, 0, users.ids)]
|
||||||
|
|
||||||
|
@api.constrains('parent_id')
|
||||||
|
def _check_hierarhcical_recursion(self):
|
||||||
|
"""
|
||||||
|
Check hierarchical recursion
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _check_recursion of models.py
|
||||||
|
"""
|
||||||
|
if not self._check_recursion():
|
||||||
|
raise ValidationError(_('You cannot create recursive KPIs.'))
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api.constrains('formula')
|
||||||
|
def _check_formula_recursion(self):
|
||||||
|
"""
|
||||||
|
Check formula recursion not to have other KPIs calculation leads to this calculation
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _check_recursion of models.py
|
||||||
|
"""
|
||||||
|
for kpi in self:
|
||||||
|
parent_kpis = kpi.kpi_ids
|
||||||
|
while parent_kpis:
|
||||||
|
if kpi.id in parent_kpis.ids:
|
||||||
|
raise ValidationError(_(
|
||||||
|
'Other KPIs used in formula depends on this KPI. Calculation is impossible'
|
||||||
|
))
|
||||||
|
parent_kpis = parent_kpis.mapped("kpi_ids")
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api.onchange("result_appearance")
|
||||||
|
def _onchange_result_appearance(self):
|
||||||
|
"""
|
||||||
|
Onchange method for result_appearance
|
||||||
|
"""
|
||||||
|
for kpi in self:
|
||||||
|
if kpi.result_appearance == "percentage":
|
||||||
|
kpi.result_suffix = "%"
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string="Name",
|
||||||
|
required=True,
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
category_id = fields.Many2one(
|
||||||
|
"kpi.category",
|
||||||
|
string="Category",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
formula = fields.Char(
|
||||||
|
string="Formula",
|
||||||
|
)
|
||||||
|
formula_warning = fields.Char(
|
||||||
|
string="Alert",
|
||||||
|
compute=_compute_measures_ids,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
line_ids = fields.One2many(
|
||||||
|
"kpi.scorecard.line",
|
||||||
|
"period_id",
|
||||||
|
string="Targets"
|
||||||
|
)
|
||||||
|
result_type = fields.Selection(
|
||||||
|
[
|
||||||
|
("more", "The more the better"),
|
||||||
|
("less", "The less the better"),
|
||||||
|
],
|
||||||
|
string="Success Criteria",
|
||||||
|
default="more",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
result_appearance = fields.Selection(
|
||||||
|
[
|
||||||
|
("number", "Number"),
|
||||||
|
("percentage", "Percentage"),
|
||||||
|
("monetory", "Monetary"),
|
||||||
|
],
|
||||||
|
string="Result Type",
|
||||||
|
default="number",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
result_suffix = fields.Char(
|
||||||
|
string="Result Suffix",
|
||||||
|
help="would be shown after the result value",
|
||||||
|
)
|
||||||
|
result_preffix = fields.Char(
|
||||||
|
string="Result Prefix",
|
||||||
|
help="would be shown before the result value",
|
||||||
|
)
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
"res.currency",
|
||||||
|
string="Currency",
|
||||||
|
default=lambda self: self.env.user.company_id.currency_id,
|
||||||
|
)
|
||||||
|
result_rounding = fields.Selection(
|
||||||
|
[
|
||||||
|
("1", "1"),
|
||||||
|
("2", "2"),
|
||||||
|
("3", "3"),
|
||||||
|
("4", "4"),
|
||||||
|
],
|
||||||
|
string="Rounding Decimals",
|
||||||
|
default="2",
|
||||||
|
)
|
||||||
|
parent_id = fields.Many2one(
|
||||||
|
"kpi.item",
|
||||||
|
string="Parent KPI",
|
||||||
|
)
|
||||||
|
all_parent_ids = fields.Many2many(
|
||||||
|
"kpi.item",
|
||||||
|
"kpi_item_kpi_item_all_parents_rel_table",
|
||||||
|
"kpi_item_all_id",
|
||||||
|
"kpi_item_all_back_id",
|
||||||
|
string="All Parents",
|
||||||
|
compute=_compute_all_parent_ids,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
child_ids = fields.One2many(
|
||||||
|
"kpi.item",
|
||||||
|
"parent_id",
|
||||||
|
string="Child KPIs",
|
||||||
|
)
|
||||||
|
measures_ids = fields.Many2many(
|
||||||
|
"kpi.measure.item",
|
||||||
|
"kpi_item_kpi_measure_rel_table"
|
||||||
|
"kpi_item_measure_rel_id",
|
||||||
|
"kpi_measure_item_rel_id",
|
||||||
|
string="Measurements",
|
||||||
|
compute=_compute_measures_ids,
|
||||||
|
context={'active_test':False},
|
||||||
|
)
|
||||||
|
constant_ids = fields.Many2many(
|
||||||
|
"kpi.constant",
|
||||||
|
"kpi_item_kpi_constant_rel_table",
|
||||||
|
"kpi_item_id",
|
||||||
|
"kpi_constant_id",
|
||||||
|
string="Constants",
|
||||||
|
compute=_compute_measures_ids,
|
||||||
|
context={'active_test':False},
|
||||||
|
)
|
||||||
|
kpi_ids = fields.Many2many(
|
||||||
|
"kpi.item",
|
||||||
|
"kpi_item_kpi_item_rel_table",
|
||||||
|
"kpi_item_this_id",
|
||||||
|
"kpi_item_back_id",
|
||||||
|
string="Other KPIs",
|
||||||
|
compute=_compute_measures_ids,
|
||||||
|
context={'active_test': False},
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
string="Company",
|
||||||
|
)
|
||||||
|
active = fields.Boolean(
|
||||||
|
string="Active",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
description = fields.Text(
|
||||||
|
string="Notes",
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
sequence = fields.Integer(string="Sequence")
|
||||||
|
user_ids = fields.Many2many(
|
||||||
|
"res.users",
|
||||||
|
"res_users_kpi_item_rel_table",
|
||||||
|
"res_user_rel_id",
|
||||||
|
"kpi_item_id",
|
||||||
|
string="Allowed Users",
|
||||||
|
)
|
||||||
|
user_group_ids = fields.Many2many(
|
||||||
|
"res.groups",
|
||||||
|
"res_groups_kpi_item_rel_table",
|
||||||
|
"res_groups_id",
|
||||||
|
"kpi_item_id",
|
||||||
|
string="Allowed User Groups",
|
||||||
|
)
|
||||||
|
access_user_ids = fields.Many2many(
|
||||||
|
"res.users",
|
||||||
|
"res_users_kpi_item_all_rel_table",
|
||||||
|
"res_user_all_rel_id",
|
||||||
|
"kpi_item_all_id",
|
||||||
|
string="Access Users",
|
||||||
|
compute=_compute_access_user_ids,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
edit_user_ids = fields.Many2many(
|
||||||
|
"res.users",
|
||||||
|
"res_users_kpi_item_edit_rel_table",
|
||||||
|
"res_user_rel_id",
|
||||||
|
"kpi_item_id",
|
||||||
|
string="Edit Rights Allowed Users",
|
||||||
|
)
|
||||||
|
edit_user_group_ids = fields.Many2many(
|
||||||
|
"res.groups",
|
||||||
|
"res_groups_kpi_item_edit_rel_table",
|
||||||
|
"res_groups_id",
|
||||||
|
"kpi_item_id",
|
||||||
|
string="Edit Rights User Groups",
|
||||||
|
)
|
||||||
|
edit_access_user_ids = fields.Many2many(
|
||||||
|
"res.users",
|
||||||
|
"res_users_kpi_item_edit_all_rel_table",
|
||||||
|
"res_user_all_rel_id",
|
||||||
|
"kpi_item_all_id",
|
||||||
|
string="Edit Rights Access Users",
|
||||||
|
compute=_compute_edit_access_user_ids,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_order = "sequence, id"
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
"""
|
||||||
|
The method to block unlink if used in any KPI
|
||||||
|
"""
|
||||||
|
for kpi in self:
|
||||||
|
kpi_key = "KPI({})".format(kpi.id)
|
||||||
|
domain = [
|
||||||
|
("formula", "like", kpi_key),
|
||||||
|
"|",
|
||||||
|
("active", "=", False),
|
||||||
|
("active", "=", True),
|
||||||
|
]
|
||||||
|
kpi_id = self.env["kpi.item"].sudo().search(domain, limit=1)
|
||||||
|
if kpi_id:
|
||||||
|
raise ValidationError(
|
||||||
|
_("There are KPIs which depend on this KPI: {}. Delete them before".format(kpi_id))
|
||||||
|
)
|
||||||
|
super(kpi_item, self).unlink()
|
||||||
|
|
||||||
|
def action_return_measures(self, formula):
|
||||||
|
"""
|
||||||
|
The method to return available measurements, constants
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* action_render_formula
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
measure_ids = self.env["kpi.measure.item"].search([('existing_kpi', '!=', False)]).mapped(lambda mes: {
|
||||||
|
"id": mes.id,
|
||||||
|
"name": mes.name,
|
||||||
|
"description": mes.description or mes.name,
|
||||||
|
})
|
||||||
|
constant_ids = self.env["kpi.constant"].search([]).mapped(lambda const: {
|
||||||
|
"id": const.id,
|
||||||
|
"name": const.name,
|
||||||
|
"description": const.description or const.name,
|
||||||
|
})
|
||||||
|
kpi_ids = self.env["kpi.item"].search([("id", "!=", self.id)]).mapped(lambda kpi: {
|
||||||
|
"id": kpi.id,
|
||||||
|
"name": kpi.name,
|
||||||
|
"description": kpi.description or kpi.name,
|
||||||
|
})
|
||||||
|
formulaparts = self.action_render_formula(formula)
|
||||||
|
return {
|
||||||
|
"measures": measure_ids,
|
||||||
|
"constants": constant_ids,
|
||||||
|
"kpis": kpi_ids,
|
||||||
|
"operands": OPERANDS,
|
||||||
|
"formulaparts": formulaparts,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_render_formula(self, formula):
|
||||||
|
"""
|
||||||
|
The method to split formula on parts
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get_id_from_string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* list of dicts
|
||||||
|
** id
|
||||||
|
** name
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* expected singleton
|
||||||
|
"""
|
||||||
|
formula_parts = []
|
||||||
|
if formula:
|
||||||
|
formula_parts_str = formula.split(";")
|
||||||
|
for part in formula_parts_str:
|
||||||
|
if part.startswith("MEASURE"):
|
||||||
|
part_id = self._get_id_from_string(part)
|
||||||
|
if part_id:
|
||||||
|
measure_id = self.env["kpi.measure.item"].browse(part_id)
|
||||||
|
formula_parts.append({
|
||||||
|
"type": "MEASURE",
|
||||||
|
"res_id": part_id,
|
||||||
|
"id": part,
|
||||||
|
"name": measure_id.exists() and cut_name_part(measure_id.name) or ERRORFORM,
|
||||||
|
"extra_class": "kpi-measure-highlight",
|
||||||
|
})
|
||||||
|
elif part.startswith("CONST"):
|
||||||
|
part_id = self._get_id_from_string(part)
|
||||||
|
if part_id:
|
||||||
|
const_id = self.env["kpi.constant"].browse(part_id)
|
||||||
|
formula_parts.append({
|
||||||
|
"type": "CONST",
|
||||||
|
"res_id": part_id,
|
||||||
|
"id": part,
|
||||||
|
"name": const_id.exists() and cut_name_part(const_id.name) or ERRORFORM,
|
||||||
|
"extra_class": "kpi-const-highlight",
|
||||||
|
})
|
||||||
|
elif part.startswith("KPI"):
|
||||||
|
part_id = self._get_id_from_string(part)
|
||||||
|
if part_id:
|
||||||
|
kpi_id = self.env["kpi.item"].browse(part_id)
|
||||||
|
formula_parts.append({
|
||||||
|
"type": "KPI",
|
||||||
|
"res_id": part_id,
|
||||||
|
"id": part,
|
||||||
|
"name": kpi_id.exists() and cut_name_part(kpi_id.name) or ERRORFORM,
|
||||||
|
"extra_class": "kpi-kpi-highlight",
|
||||||
|
})
|
||||||
|
elif part.startswith("PERIOD"):
|
||||||
|
if part == "PERIOD_len":
|
||||||
|
formula_parts.append({
|
||||||
|
"type": "PERIOD",
|
||||||
|
"res_id": False,
|
||||||
|
"id": part,
|
||||||
|
"name": _("Period Days"),
|
||||||
|
"extra_class": "kpi-measure-highlight",
|
||||||
|
})
|
||||||
|
elif part == "PERIOD_passed":
|
||||||
|
formula_parts.append({
|
||||||
|
"type": "PERIOD_passed",
|
||||||
|
"res_id": False,
|
||||||
|
"id": part,
|
||||||
|
"name": _("Days Passed"),
|
||||||
|
"extra_class": "kpi-measure-highlight",
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
formula_parts.append({
|
||||||
|
"type": "OPERATOR",
|
||||||
|
"res_id": False,
|
||||||
|
"id": part,
|
||||||
|
"name": part,
|
||||||
|
"extra_class": "kpi-element-operator kpi-operator-highlight",
|
||||||
|
})
|
||||||
|
return formula_parts
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_open_formula_part(self, formula_part_id):
|
||||||
|
"""
|
||||||
|
The method to open formula part (measure, cosntant, another kpi)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* formula_part_id - char
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get_id_from_string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* action dict (or False if not connected)
|
||||||
|
"""
|
||||||
|
res_id = False
|
||||||
|
if formula_part_id:
|
||||||
|
action = {
|
||||||
|
"name": _("Details"),
|
||||||
|
"view_mode": "form",
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"views": [(False, 'form')],
|
||||||
|
"target": "new",
|
||||||
|
}
|
||||||
|
if formula_part_id.startswith("MEASURE"):
|
||||||
|
res_id = self._get_id_from_string(formula_part_id)
|
||||||
|
action.update({
|
||||||
|
"res_id": res_id,
|
||||||
|
"res_model": "kpi.measure.item",
|
||||||
|
})
|
||||||
|
elif formula_part_id.startswith("CONST"):
|
||||||
|
res_id = self._get_id_from_string(formula_part_id)
|
||||||
|
action.update({
|
||||||
|
"res_id": res_id,
|
||||||
|
"res_model": "kpi.constant",
|
||||||
|
})
|
||||||
|
elif formula_part_id.startswith("KPI"):
|
||||||
|
res_id = self._get_id_from_string(formula_part_id)
|
||||||
|
action.update({
|
||||||
|
"res_id": res_id,
|
||||||
|
"res_model": "kpi.item",
|
||||||
|
})
|
||||||
|
return res_id and action or False
|
||||||
|
|
||||||
|
def _calculate_by_measure(self, all_variables_dict):
|
||||||
|
"""
|
||||||
|
The method to consturct formula with numeric values and calculate the result
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* float
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
try:
|
||||||
|
if self.formula:
|
||||||
|
numeric_formula = self.formula.replace(";", " ")
|
||||||
|
for key, value in all_variables_dict.items():
|
||||||
|
if numeric_formula.find(key) != -1:
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
numeric_formula = numeric_formula.replace(key, str(value))
|
||||||
|
else:
|
||||||
|
res = value
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
res = safe_eval(numeric_formula)
|
||||||
|
else:
|
||||||
|
res = _("Computation Error: the formula is empty")
|
||||||
|
except Exception as e:
|
||||||
|
res = _("Computation Error: {}".format(e))
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _get_measures(self):
|
||||||
|
"""
|
||||||
|
The method to recursively return included measures and constants
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* kpi.measure.item recordset
|
||||||
|
* kpi.constant recordset
|
||||||
|
"""
|
||||||
|
all_measures = self.mapped("measures_ids")
|
||||||
|
all_constants = self.mapped("constant_ids")
|
||||||
|
all_other_kpis = self.mapped("kpi_ids")
|
||||||
|
if all_other_kpis:
|
||||||
|
child_measures, child_constants, child_other_kpis = all_other_kpis._get_measures()
|
||||||
|
all_measures += child_measures
|
||||||
|
all_constants += child_constants
|
||||||
|
all_other_kpis += child_other_kpis
|
||||||
|
return all_measures, all_constants, all_other_kpis
|
||||||
|
|
||||||
|
def _check_formula_with_testing_data(self):
|
||||||
|
"""
|
||||||
|
The method to check the formula with random tesing data
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _calculate_by_measure
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* False if everything is fine
|
||||||
|
* char - error messages otherwise
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
res = False
|
||||||
|
if self.formula:
|
||||||
|
all_variables_dict = {}
|
||||||
|
for measure in self.measures_ids:
|
||||||
|
if measure.existing_kpi != False:
|
||||||
|
res = _("The formula uses obsolete measurement: {}".format(measure.name))
|
||||||
|
break
|
||||||
|
all_variables_dict.update({"MEASURE({})".format(measure.id): random.randint(5, 10000)})
|
||||||
|
else:
|
||||||
|
for constant in self.constant_ids:
|
||||||
|
all_variables_dict.update({"CONST({})".format(constant.id): random.randint(5, 10000)})
|
||||||
|
else:
|
||||||
|
all_variables_dict.update({
|
||||||
|
"PERIOD_len": 90,
|
||||||
|
"PERIOD_passed": 80,
|
||||||
|
})
|
||||||
|
for kpi in self.kpi_ids:
|
||||||
|
child_check = kpi._check_formula_with_testing_data()
|
||||||
|
if child_check:
|
||||||
|
res = _("The formula might rely upon incorrect KPI: {}.>> {}".format(kpi.name, child_check))
|
||||||
|
break
|
||||||
|
all_variables_dict.update({"KPI({})".format(kpi.id): random.randint(5, 10000)})
|
||||||
|
else:
|
||||||
|
value = self._calculate_by_measure(all_variables_dict)
|
||||||
|
if not isinstance(value, (int, float)):
|
||||||
|
res = _("The formula might be incorrect: {}".format(value))
|
||||||
|
else:
|
||||||
|
res = _("The formula is empty")
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_id_from_string(self, str_id):
|
||||||
|
"""
|
||||||
|
The method to retrieve int from string
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* str_id - char
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* int
|
||||||
|
"""
|
||||||
|
all_numbers = re.findall('\d+', str_id)
|
||||||
|
return all_numbers and int(all_numbers[0]) or False
|
||||||
|
|
||||||
|
def _return_formated_appearance(self, value, novalue_change=False, currency=False):
|
||||||
|
"""
|
||||||
|
The method to format value accroding to KPI setting
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* value - float
|
||||||
|
* novalue_change - whether value should not be recalculated (e.g for % of target value)
|
||||||
|
* currency - res.currency object
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _return_rounded_value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* str
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
formatted_value = self._return_rounded_value(value, novalue_change)
|
||||||
|
formatted_value = isinstance(formatted_value, (int, float)) and "{:,}".format(formatted_value) \
|
||||||
|
or formatted_value
|
||||||
|
currency = self.currency_id or currency
|
||||||
|
if self.result_appearance in ["monetory"] and currency:
|
||||||
|
currency_symbol = currency.symbol
|
||||||
|
formatted_value = currency.position == "before" and "{}{}".format(currency_symbol, formatted_value) \
|
||||||
|
or "{}{}".format(formatted_value, currency_symbol)
|
||||||
|
else:
|
||||||
|
formatted_value = "{}{}{}".format(
|
||||||
|
self.result_preffix or "", formatted_value, self.result_suffix or "",
|
||||||
|
)
|
||||||
|
return formatted_value
|
||||||
|
|
||||||
|
def _return_rounded_value(self, value, novalue_change):
|
||||||
|
"""
|
||||||
|
The method to return rounded value
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* value - float
|
||||||
|
* novalue_change - whether value should not be recalculated (e.g for % of target value)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* int. float, or str
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
result_appearance = self.result_appearance
|
||||||
|
formatted_value = value
|
||||||
|
if result_appearance in ["number", "percentage", "monetory"]:
|
||||||
|
rounding = self.result_rounding and int(self.result_rounding) or 0
|
||||||
|
formatted_value = result_appearance in ["number", "monetory"] and formatted_value \
|
||||||
|
or result_appearance in ["percentage"] and novalue_change and formatted_value \
|
||||||
|
or formatted_value * 100 \
|
||||||
|
or formatted_value
|
||||||
|
formatted_value = round(formatted_value, rounding)
|
||||||
|
if rounding == 0:
|
||||||
|
formatted_value = int(formatted_value)
|
||||||
|
return formatted_value
|
||||||
|
|
||||||
|
def _return_xls_formatting(self, value, novalue_change=False):
|
||||||
|
"""
|
||||||
|
The method to format value accroding to KPI setting for table cell
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* value - float
|
||||||
|
* novalue_change - whether value should not be recalculated (e.g for % of target value)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* int or float
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
result_appearance = self.result_appearance
|
||||||
|
rounding = self.result_rounding and int(self.result_rounding) or 0
|
||||||
|
formatted_value = value
|
||||||
|
if result_appearance in ["number", "percentage", "monetory"]:
|
||||||
|
formatted_value = result_appearance in ["number", "monetory"] and formatted_value \
|
||||||
|
or result_appearance in ["percentage"] and novalue_change and formatted_value \
|
||||||
|
or formatted_value / 100 \
|
||||||
|
or formatted_value
|
||||||
|
formatted_value = round(formatted_value, rounding)
|
||||||
|
if rounding == 0:
|
||||||
|
formatted_value = int(formatted_value)
|
||||||
|
return formatted_value
|
||||||
|
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
import numbers
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tools.safe_eval import safe_eval
|
||||||
|
|
||||||
|
DEFAULT_PYTHON_CODE = """# Make sure the 'result' is defined and it contains the new current value.
|
||||||
|
# Result should be of type integer or float
|
||||||
|
# For example, result = env["crm.lead"].search_count([('create_date', '>=', period_start+timedelta(days=1))])
|
||||||
|
# Available variables:
|
||||||
|
# - env: Odoo Environment on which the action is triggered
|
||||||
|
# - period_start, period_end - period on which the KPI is calculated
|
||||||
|
# - period_company_id - res.company object for which KPI target is calculated
|
||||||
|
# - date, datetime, timedelta: useful Python libraries
|
||||||
|
result = 10\n\n\n\n"""
|
||||||
|
|
||||||
|
EXISTING_INSTALLED = _("In order to activate the KPI, please install the module for {}")
|
||||||
|
|
||||||
|
class kpi_measure(models.Model):
|
||||||
|
"""
|
||||||
|
The core model of the module to calculate KPI figure
|
||||||
|
|
||||||
|
* The idea of having computed and inversed model, date_field_id is to have prepared classifier even the linked
|
||||||
|
apps are not installed
|
||||||
|
"""
|
||||||
|
_name = "kpi.measure"
|
||||||
|
_inherit = "kpi.help"
|
||||||
|
_description = "KPI Measurement"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _return_model(self):
|
||||||
|
"""
|
||||||
|
The method to return available models
|
||||||
|
"""
|
||||||
|
self._cr.execute("SELECT model, name FROM ir_model ORDER BY name")
|
||||||
|
return self._cr.fetchall()
|
||||||
|
|
||||||
|
@api.depends("model_name")
|
||||||
|
def _compute_model_id(self):
|
||||||
|
"""
|
||||||
|
Compute method for model_id
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _return_model
|
||||||
|
* _get_id of ir.model
|
||||||
|
"""
|
||||||
|
all_models = [model_n[0] for model_n in self._return_model()]
|
||||||
|
for measure in self:
|
||||||
|
model_id = False
|
||||||
|
if measure.model_name and measure.model_name in all_models:
|
||||||
|
model_id = self.env["ir.model"].sudo().search([("model", "=", measure.model_name)], limit=1)
|
||||||
|
measure.model_id = model_id
|
||||||
|
|
||||||
|
@api.depends("date_field_name")
|
||||||
|
def _compute_date_field_ids(self):
|
||||||
|
"""
|
||||||
|
Compute method for date_field_name
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get of ir.model.fields
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
res_ids = []
|
||||||
|
all_field_names = measure.date_field_name
|
||||||
|
existing_model = not measure.existing_kpi
|
||||||
|
if all_field_names:
|
||||||
|
all_fields = all_field_names.split(",")
|
||||||
|
for field_date in all_fields:
|
||||||
|
field_id = existing_model and self.env["ir.model.fields"]._get(measure.model_name, field_date) \
|
||||||
|
or False
|
||||||
|
if field_id:
|
||||||
|
res_ids.append(field_id.id)
|
||||||
|
measure.date_field_ids = [(6, 0, res_ids)]
|
||||||
|
|
||||||
|
@api.depends("measure_field_name")
|
||||||
|
def _compute_measure_field_id(self):
|
||||||
|
"""
|
||||||
|
Compute method for measure_field_id
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get of ir.model.fields
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
field_id = not measure.existing_kpi \
|
||||||
|
and self.env["ir.model.fields"]._get(measure.model_name, measure.measure_field_name) or False
|
||||||
|
measure.measure_field_id = field_id or False
|
||||||
|
|
||||||
|
@api.depends("company_field_name")
|
||||||
|
def _compute_company_field_id(self):
|
||||||
|
"""
|
||||||
|
Compute method for company_field_id
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get of ir.model.fields
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
field_id = not measure.existing_kpi \
|
||||||
|
and self.env["ir.model.fields"]._get(measure.model_name, measure.company_field_name) or False
|
||||||
|
measure.company_field_id = field_id or False
|
||||||
|
|
||||||
|
@api.depends("model_name")
|
||||||
|
def _compute_existing_kpi(self):
|
||||||
|
"""
|
||||||
|
Compute method for existing_kpi
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
res = False
|
||||||
|
if measure.measure_type in ["sum", "average", "count"] and measure.model_name and not measure.model_id:
|
||||||
|
res = EXISTING_INSTALLED.format(measure.model_name)
|
||||||
|
measure.existing_kpi = res
|
||||||
|
|
||||||
|
@api.depends("item_ids")
|
||||||
|
def _compute_measures_len(self):
|
||||||
|
"""
|
||||||
|
Compute method for measures_len
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
measure.measures_len = len(measure.item_ids)
|
||||||
|
|
||||||
|
def _inverse_model_id(self):
|
||||||
|
"""
|
||||||
|
Inverse method for model_id
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
measure.model_name = measure.model_id and measure.model_id.model or False
|
||||||
|
|
||||||
|
def _inverse_date_field_ids(self):
|
||||||
|
"""
|
||||||
|
Inverse method for date_field_ids
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
measure.date_field_name = measure.date_field_ids and ",".join(measure.date_field_ids.mapped("name")) or \
|
||||||
|
False
|
||||||
|
|
||||||
|
def _inverse_measure_field_id(self):
|
||||||
|
"""
|
||||||
|
Inverse method for measure_field_id
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
measure.measure_field_name = measure.measure_field_id and measure.measure_field_id.name or False
|
||||||
|
|
||||||
|
def _inverse_company_field_id(self):
|
||||||
|
"""
|
||||||
|
Inverse method for company_field_id
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
measure.company_field_name = measure.company_field_id and measure.company_field_id.name or False
|
||||||
|
|
||||||
|
@api.onchange("model_id")
|
||||||
|
def _onchange_model_id(self):
|
||||||
|
"""
|
||||||
|
Onchange method for model_id - to update fields and
|
||||||
|
"""
|
||||||
|
for measure in self:
|
||||||
|
company_field_id = False
|
||||||
|
if measure.model_id:
|
||||||
|
measure.model_name = measure.model_id.model
|
||||||
|
company_field = self.env["ir.model.fields"]._get(measure.model_id.model, "company_id")
|
||||||
|
if company_field:
|
||||||
|
company_field_id = company_field
|
||||||
|
measure.date_field_ids = False
|
||||||
|
measure.measure_field_id = False
|
||||||
|
measure.company_field_id = company_field_id
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def search_existing_kpi(self, operator, value):
|
||||||
|
"""
|
||||||
|
Search method for existing_kpi
|
||||||
|
"""
|
||||||
|
all_measures = self.search([])
|
||||||
|
measure_ids = []
|
||||||
|
for measure in all_measures:
|
||||||
|
res = False
|
||||||
|
if measure.measure_type in ["sum", "average", "count"] and measure.model_name and not measure.model_id:
|
||||||
|
res = EXISTING_INSTALLED.format(measure.model_name)
|
||||||
|
if value == False:
|
||||||
|
if operator == "!=" and not res:
|
||||||
|
measure_ids.append(measure.id)
|
||||||
|
elif operator == "=" and res:
|
||||||
|
measure_ids.append(measure.id)
|
||||||
|
elif res and res.find(value) != -1:
|
||||||
|
measure_ids.append(measure.id)
|
||||||
|
return [('id', 'in', measure_ids)]
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string="Name",
|
||||||
|
required=True,
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
measure_type = fields.Selection(
|
||||||
|
[
|
||||||
|
("sum", "Sum of records field"),
|
||||||
|
("average", "Average of records field"),
|
||||||
|
("count", "Count of records"),
|
||||||
|
("py_code", "Execute Python code"),
|
||||||
|
],
|
||||||
|
string="KPI Type",
|
||||||
|
default="sum",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
py_code = fields.Text(
|
||||||
|
string="Python Code",
|
||||||
|
default=DEFAULT_PYTHON_CODE,
|
||||||
|
help="Make sure the 'result' is defined and it contains the new current value."
|
||||||
|
)
|
||||||
|
model_name = fields.Char("Model Name")
|
||||||
|
model_id = fields.Many2one(
|
||||||
|
'ir.model',
|
||||||
|
string='Model',
|
||||||
|
compute=_compute_model_id,
|
||||||
|
inverse=_inverse_model_id,
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
domain = fields.Text(
|
||||||
|
string="Filters",
|
||||||
|
default="[]",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
date_field_name = fields.Char(string="Date Field Name",)
|
||||||
|
date_field_ids = fields.Many2many(
|
||||||
|
"ir.model.fields",
|
||||||
|
"ir_model_fields_kpi_measure_rel_table",
|
||||||
|
"ir_model_field_date_id",
|
||||||
|
"kpi_measure_rel_id",
|
||||||
|
string='Date Fields',
|
||||||
|
compute=_compute_date_field_ids,
|
||||||
|
inverse=_inverse_date_field_ids,
|
||||||
|
readonly=False,
|
||||||
|
help="""
|
||||||
|
According to those dates Odoo will calculate whether a record is within a specified period.
|
||||||
|
In case there are a few of date fields, all of related dates should be within a period.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
measure_field_name = fields.Char(
|
||||||
|
string="Measure Field Name",
|
||||||
|
)
|
||||||
|
measure_field_id = fields.Many2one(
|
||||||
|
'ir.model.fields',
|
||||||
|
string='Measure Field',
|
||||||
|
compute=_compute_measure_field_id,
|
||||||
|
inverse=_inverse_measure_field_id,
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
company_field_name = fields.Char(
|
||||||
|
string="Company Field Name",
|
||||||
|
)
|
||||||
|
company_field_id = fields.Many2one(
|
||||||
|
'ir.model.fields',
|
||||||
|
string='Company Field',
|
||||||
|
compute=_compute_company_field_id,
|
||||||
|
inverse=_inverse_company_field_id,
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
existing_kpi = fields.Char(
|
||||||
|
string="Installed",
|
||||||
|
compute=_compute_existing_kpi,
|
||||||
|
search="search_existing_kpi",
|
||||||
|
)
|
||||||
|
item_ids = fields.One2many(
|
||||||
|
"kpi.measure.item",
|
||||||
|
"measure_id",
|
||||||
|
string="Measurements"
|
||||||
|
)
|
||||||
|
measures_len = fields.Integer(
|
||||||
|
string="Measurements Count",
|
||||||
|
compute=_compute_measures_len,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
active = fields.Boolean(
|
||||||
|
string="Active",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
description = fields.Text(
|
||||||
|
string="Notes",
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
sequence = fields.Integer(string="Sequence")
|
||||||
|
|
||||||
|
_order = "sequence, id"
|
||||||
|
|
||||||
|
def _execute_py_code(self, date_start, date_end, period_company_id):
|
||||||
|
"""
|
||||||
|
The method to executr Python code
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* date_start - date
|
||||||
|
* date_end - date
|
||||||
|
* period_company_id - res.company object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* float or integer - if success
|
||||||
|
* char - if error
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
cxt = {
|
||||||
|
'env': self.env,
|
||||||
|
'period_start': date_start,
|
||||||
|
'period_end': date_end,
|
||||||
|
"period_company_id": period_company_id,
|
||||||
|
'date': date,
|
||||||
|
'datetime': datetime,
|
||||||
|
'timedelta': timedelta,
|
||||||
|
}
|
||||||
|
code = self.py_code.strip()
|
||||||
|
try:
|
||||||
|
safe_eval(code, cxt, mode="exec", nocopy=True)
|
||||||
|
result = cxt.get('result')
|
||||||
|
if result is None:
|
||||||
|
result = _("Computation Error: Python code is incorrect: it doesn't contain 'result' key word")
|
||||||
|
elif not isinstance(result, (int, float)):
|
||||||
|
result = _(
|
||||||
|
"Computation Error: Python code is incorrect: it returns not number but {}".format(type(result))
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
result = _("Computation Error: {}".format(e))
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tools.safe_eval import safe_eval
|
||||||
|
|
||||||
|
|
||||||
|
class kpi_measure_item(models.Model):
|
||||||
|
"""
|
||||||
|
The key model for KPI calulation
|
||||||
|
"""
|
||||||
|
_name = "kpi.measure.item"
|
||||||
|
_inherit = "kpi.help"
|
||||||
|
_description = "KPI Measurement (Variable)"
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string="Name",
|
||||||
|
required=True,
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
measure_id = fields.Many2one(
|
||||||
|
"kpi.measure",
|
||||||
|
string="Measurement",
|
||||||
|
required=True,
|
||||||
|
domain=[("existing_kpi", "!=", False)],
|
||||||
|
)
|
||||||
|
domain = fields.Text(
|
||||||
|
string="Extra Filters",
|
||||||
|
default="[]",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
model_id = fields.Many2one(
|
||||||
|
related="measure_id.model_id",
|
||||||
|
store=True,
|
||||||
|
compute_sudo=True,
|
||||||
|
related_sudo=True,
|
||||||
|
)
|
||||||
|
model_name = fields.Char(
|
||||||
|
related="measure_id.model_name",
|
||||||
|
store=True,
|
||||||
|
compute_sudo=True,
|
||||||
|
related_sudo=True,
|
||||||
|
)
|
||||||
|
measure_type = fields.Selection(
|
||||||
|
related="measure_id.measure_type",
|
||||||
|
store=True,
|
||||||
|
compute_sudo=True,
|
||||||
|
related_sudo=True,
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
string="Company"
|
||||||
|
)
|
||||||
|
active = fields.Boolean(
|
||||||
|
string="Active",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
description = fields.Text(
|
||||||
|
string="Notes",
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
sequence = fields.Integer(string="Sequence")
|
||||||
|
existing_kpi = fields.Char(related="measure_id.existing_kpi",)
|
||||||
|
|
||||||
|
_order = "sequence, id"
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
"""
|
||||||
|
The method to block unlink if used in any KPI
|
||||||
|
"""
|
||||||
|
for kpi in self:
|
||||||
|
measure_item_key = "MEASURE({})".format(kpi.id)
|
||||||
|
domain = [
|
||||||
|
("formula", "like", measure_item_key),
|
||||||
|
"|",
|
||||||
|
("active", "=", False),
|
||||||
|
("active", "=", True),
|
||||||
|
]
|
||||||
|
kpi_id = self.env["kpi.item"].sudo().search(domain, limit=1)
|
||||||
|
if kpi_id:
|
||||||
|
raise ValidationError(
|
||||||
|
_("There are KPIs which depend on this MEASUREMENT: {}. Delete them before".format(kpi_id))
|
||||||
|
)
|
||||||
|
super(kpi_measure_item, self).unlink()
|
||||||
|
|
||||||
|
def _calculate_for_period(self, period_id):
|
||||||
|
"""
|
||||||
|
The method to calculate KPI for period
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* period_id - kpi.period
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* float
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
self = self.sudo()
|
||||||
|
measure = self.measure_id
|
||||||
|
res = _("""Computation Error: KPI is not correctly defined. Perhaps, the related module was uninstalled or
|
||||||
|
field was removed. Please check basic measurements involved in formula""")
|
||||||
|
date_start = period_id.date_start
|
||||||
|
date_end = period_id.date_end
|
||||||
|
company_id = period_id.company_id
|
||||||
|
if measure.measure_type == "py_code":
|
||||||
|
res = measure._execute_py_code(date_start, date_end, company_id)
|
||||||
|
elif measure.model_id:
|
||||||
|
considered_model = self.env[measure.model_name]
|
||||||
|
try:
|
||||||
|
domain = safe_eval(self.domain)+safe_eval(measure.domain)
|
||||||
|
for date_field in measure.date_field_ids:
|
||||||
|
domain += [
|
||||||
|
(date_field.name, ">=", date_start),
|
||||||
|
(date_field.name, "<=", date_end),
|
||||||
|
]
|
||||||
|
if measure.company_field_id:
|
||||||
|
company_f = measure.company_field_name
|
||||||
|
domain += ["|", (company_f, "=", False), (company_f, "=", company_id.id)]
|
||||||
|
if measure.measure_type == "count":
|
||||||
|
res = considered_model.search_count(domain)
|
||||||
|
elif measure.measure_field_name:
|
||||||
|
if measure.measure_type == "sum":
|
||||||
|
res = sum(considered_model.search(domain).mapped(measure.measure_field_name))
|
||||||
|
elif measure.measure_type == "average":
|
||||||
|
all_records = considered_model.search(domain)
|
||||||
|
res = sum(all_records.mapped(measure.measure_field_name)) / len(all_records)
|
||||||
|
except Exception as e:
|
||||||
|
res = """{}
|
||||||
|
{}""".format(res, e)
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,391 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import xlsxwriter
|
||||||
|
except ImportError:
|
||||||
|
_logger.warning("Cannot import xlsxwriter")
|
||||||
|
xlsxwriter = False
|
||||||
|
|
||||||
|
|
||||||
|
class kpi_period(models.Model):
|
||||||
|
"""
|
||||||
|
The model to distinguish KPIs objects by periods
|
||||||
|
"""
|
||||||
|
_name = "kpi.period"
|
||||||
|
_inherit = "kpi.help"
|
||||||
|
_description = "KPI Period"
|
||||||
|
|
||||||
|
@api.depends("date_start", "date_end")
|
||||||
|
def _compute_period_length(self):
|
||||||
|
"""
|
||||||
|
Compute method for period_length
|
||||||
|
"""
|
||||||
|
for period in self:
|
||||||
|
period_length = 0
|
||||||
|
if period.date_end and period.date_start:
|
||||||
|
period_length = (period.date_end - period.date_start).days + 1
|
||||||
|
period.period_length = period_length
|
||||||
|
|
||||||
|
@api.depends("date_start", "date_end")
|
||||||
|
def _compute_period_passed(self):
|
||||||
|
"""
|
||||||
|
Compute method for period_passed
|
||||||
|
"""
|
||||||
|
today = fields.Date.today()
|
||||||
|
for period in self:
|
||||||
|
period_passed = 1
|
||||||
|
if period.date_end and period.date_start:
|
||||||
|
if period.date_end <= today:
|
||||||
|
period_passed = period.period_length
|
||||||
|
elif period.date_start < today:
|
||||||
|
period_passed = (today - period.date_start).days + 1
|
||||||
|
period.period_passed = period_passed
|
||||||
|
|
||||||
|
def _compute_parent_id(self):
|
||||||
|
"""
|
||||||
|
Compute method for parent_id (e.g. for montly it would be quartely if exists or annually)
|
||||||
|
"""
|
||||||
|
for period in self:
|
||||||
|
parent_period = False
|
||||||
|
if period.date_end and period.date_start:
|
||||||
|
parent_period = self.search([
|
||||||
|
("date_start", "!=", False),
|
||||||
|
("date_end", "!=", False),
|
||||||
|
("id", "!=", period.id),
|
||||||
|
("date_start", "<=", period.date_start),
|
||||||
|
("date_end", ">=", period.date_end),
|
||||||
|
], limit=1, order="period_length ASC, id")
|
||||||
|
period.parent_id = parent_period
|
||||||
|
|
||||||
|
def _inverse_template_id(self):
|
||||||
|
"""
|
||||||
|
Inverse method for template_id
|
||||||
|
"""
|
||||||
|
for period in self:
|
||||||
|
if period.template_id:
|
||||||
|
line_vals = []
|
||||||
|
for line in period.template_id.line_ids:
|
||||||
|
if line.kpi_id.active:
|
||||||
|
line_vals.append((0, 0, {
|
||||||
|
"kpi_id": line.kpi_id.id,
|
||||||
|
"target_value": line.target_value,
|
||||||
|
}))
|
||||||
|
if line_vals:
|
||||||
|
period.line_ids = line_vals
|
||||||
|
period.template_id = False
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string="Reference",
|
||||||
|
required=True,
|
||||||
|
translate=True,
|
||||||
|
)
|
||||||
|
date_start = fields.Date(
|
||||||
|
"Period Start",
|
||||||
|
required=True,
|
||||||
|
default=lambda self: fields.Date.today().strftime('%Y-01-01'),
|
||||||
|
)
|
||||||
|
date_end = fields.Date(
|
||||||
|
"Period End",
|
||||||
|
required=True,
|
||||||
|
default=lambda self: fields.Date.today().strftime('%Y-12-31'),
|
||||||
|
)
|
||||||
|
line_ids = fields.One2many(
|
||||||
|
"kpi.scorecard.line",
|
||||||
|
"period_id",
|
||||||
|
string="KPI Targets",
|
||||||
|
copy=True,
|
||||||
|
)
|
||||||
|
period_length = fields.Float(
|
||||||
|
string="Period Days",
|
||||||
|
compute=_compute_period_length,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
period_passed = fields.Float(
|
||||||
|
string="Days Passed",
|
||||||
|
compute=_compute_period_passed,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
parent_id = fields.Many2one(
|
||||||
|
"kpi.period",
|
||||||
|
string="Parent Period",
|
||||||
|
compute=_compute_parent_id,
|
||||||
|
)
|
||||||
|
state = fields.Selection(
|
||||||
|
[
|
||||||
|
("open", "Opened"),
|
||||||
|
("closed", "Closed"),
|
||||||
|
],
|
||||||
|
string="State",
|
||||||
|
default="open",
|
||||||
|
)
|
||||||
|
template_id = fields.Many2one(
|
||||||
|
"kpi.period",
|
||||||
|
string="Copy targets from",
|
||||||
|
inverse=_inverse_template_id,
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
string="Company",
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env.user.company_id,
|
||||||
|
)
|
||||||
|
last_recalculation_date = fields.Datetime(
|
||||||
|
string="Last KPI Calculation",
|
||||||
|
default=lambda self: fields.Datetime.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_order = "date_start DESC, period_length, id"
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
'dates_check',
|
||||||
|
'check (date_end>=date_start)',
|
||||||
|
_('Period end should be after period start')
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
"""
|
||||||
|
Overloading the method to include period start and end ino the names
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
lang = self._context.get("lang")
|
||||||
|
lang_date_format = "%m/%d/%Y"
|
||||||
|
if lang:
|
||||||
|
record_lang = self.env['res.lang'].search([("code", "=", lang)], limit=1)
|
||||||
|
lang_date_format = record_lang.date_format
|
||||||
|
for period in self:
|
||||||
|
date_start = period.date_start.strftime(lang_date_format)
|
||||||
|
date_end = period.date_end.strftime(lang_date_format)
|
||||||
|
name = "{} ({} - {})".format(period.name, date_start, date_end)
|
||||||
|
result.append((period.id, name))
|
||||||
|
return result
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_return_periods(self):
|
||||||
|
"""
|
||||||
|
The method to return available periods
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* dict
|
||||||
|
** all_periods - list of tuples
|
||||||
|
** this_period - int - id
|
||||||
|
"""
|
||||||
|
all_periods = self.search([])
|
||||||
|
today = fields.Date.today()
|
||||||
|
this_period = False
|
||||||
|
if all_periods:
|
||||||
|
this_periods = all_periods.filtered(lambda period: period.date_start <= today and period.date_end >= today)
|
||||||
|
this_periods = this_periods.sorted("period_length", reverse=False)
|
||||||
|
this_period = this_periods and this_periods[0].id or all_periods[0].id
|
||||||
|
return {
|
||||||
|
"all_periods": all_periods and all_periods.mapped(lambda per: [per.id, per.name_get()[0][1], per.state]) \
|
||||||
|
or False,
|
||||||
|
"this_period": this_period,
|
||||||
|
"kpi_adming_rights": self.env.user.has_group("kpi_scorecard.group_kpi_admin"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_calculate_kpis(self):
|
||||||
|
"""
|
||||||
|
The method to update KPI values
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _calculate_kpis
|
||||||
|
"""
|
||||||
|
for period in self:
|
||||||
|
period._calculate_kpis()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_cron_calculate_kpi(self):
|
||||||
|
"""
|
||||||
|
The method to find all periods for re-calculations
|
||||||
|
"""
|
||||||
|
open_periods = self.search([("state", "=", "open")], order="last_recalculation_date ASC, id")
|
||||||
|
open_periods.action_calculate_kpis()
|
||||||
|
|
||||||
|
def action_export_scorecard(self):
|
||||||
|
"""
|
||||||
|
The method to prepare the xls table
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get_xls_table of kpi.scorecard.line
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* action of downloading the xlsx table
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if not xlsxwriter:
|
||||||
|
raise UserError(_("The Python library xlsxwriter is installed. Contact your system administrator"))
|
||||||
|
file_name = u"{}.xlsx".format(self.name_get()[0][1])
|
||||||
|
file_path = tempfile.mktemp(suffix='.xlsx')
|
||||||
|
workbook = xlsxwriter.Workbook(file_path)
|
||||||
|
main_header_style = workbook.add_format({
|
||||||
|
'bold': True,
|
||||||
|
'font_size': 11,
|
||||||
|
'border': 1,
|
||||||
|
'align': 'center',
|
||||||
|
'valign': 'vcenter',
|
||||||
|
'bg_color': 'silver',
|
||||||
|
'border_color': 'gray',
|
||||||
|
})
|
||||||
|
main_cell_style_dict = {
|
||||||
|
'font_size': 11,
|
||||||
|
'border': 1,
|
||||||
|
'border_color': 'gray',
|
||||||
|
}
|
||||||
|
worksheet = workbook.add_worksheet(file_name)
|
||||||
|
column_keys = [
|
||||||
|
{"key": "A", "label": _("KPI"), "width": 60},
|
||||||
|
{"key": "B", "label": _("Target"), "width": 14},
|
||||||
|
{"key": "C", "label": _("Actual"), "width": 14},
|
||||||
|
{"key": "D", "label": _("Notes"), "width": 80},
|
||||||
|
]
|
||||||
|
total_row_number = len(self.line_ids)
|
||||||
|
cell_values = self.line_ids._get_xls_table()
|
||||||
|
for ccolumn in column_keys:
|
||||||
|
ckey = ccolumn.get("key")
|
||||||
|
# set columns
|
||||||
|
worksheet.set_column('{c}:{c}'.format(c=ckey), ccolumn.get("width"))
|
||||||
|
# set header row
|
||||||
|
worksheet.write("{}1".format(ckey), ccolumn.get("label"), main_header_style)
|
||||||
|
# set column values
|
||||||
|
for row_number in range(2, total_row_number+2):
|
||||||
|
cell_number = "{}{}".format(ckey, row_number)
|
||||||
|
cell_value_dict = cell_values.get(cell_number)
|
||||||
|
cell_value = ""
|
||||||
|
cell_level = 0
|
||||||
|
cell_style = main_cell_style_dict.copy()
|
||||||
|
if cell_value_dict:
|
||||||
|
cell_value = cell_value_dict.get("value")
|
||||||
|
cell_style.update(cell_value_dict.get("style"))
|
||||||
|
cell_level = cell_value_dict.get("level") or 0
|
||||||
|
cell_style = workbook.add_format(cell_style)
|
||||||
|
if ckey == "A":
|
||||||
|
cell_style.set_indent(cell_level)
|
||||||
|
worksheet.write(
|
||||||
|
cell_number,
|
||||||
|
cell_value,
|
||||||
|
cell_style,
|
||||||
|
)
|
||||||
|
worksheet.set_row(0, 24)
|
||||||
|
workbook.close()
|
||||||
|
with open(file_path, 'rb') as r:
|
||||||
|
xls_file = base64.b64encode(r.read())
|
||||||
|
att_vals = {
|
||||||
|
'name': file_name,
|
||||||
|
'type': 'binary',
|
||||||
|
'datas': xls_file,
|
||||||
|
}
|
||||||
|
attachment_id = self.env['ir.attachment'].create(att_vals)
|
||||||
|
self.env.cr.commit()
|
||||||
|
action = {
|
||||||
|
'type': 'ir.actions.act_url',
|
||||||
|
'url': '/web/content/{}?download=true'.format(attachment_id.id,),
|
||||||
|
'target': 'self',
|
||||||
|
}
|
||||||
|
return action
|
||||||
|
|
||||||
|
def action_close(self):
|
||||||
|
"""
|
||||||
|
The method to close periods
|
||||||
|
"""
|
||||||
|
for period in self:
|
||||||
|
if period.state != "closed":
|
||||||
|
period.state = "closed"
|
||||||
|
|
||||||
|
def action_reopen(self):
|
||||||
|
"""
|
||||||
|
The method to close periods
|
||||||
|
"""
|
||||||
|
for period in self:
|
||||||
|
if period.state != "open":
|
||||||
|
period.state = "open"
|
||||||
|
|
||||||
|
def _calculate_kpis(self):
|
||||||
|
"""
|
||||||
|
The method to calculate KPIs for this period
|
||||||
|
1. Firstly calculate all variables to avoid duplicated calculations
|
||||||
|
2. Calculate all involved kpi.items
|
||||||
|
3. Get actual value from calculated dict and save it to line
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get_measures of kpi.item
|
||||||
|
* _calculate_for_period of kpi.measure.item
|
||||||
|
* _get_value_by_period of kpi.constant
|
||||||
|
* _calculate_by_measure of kpi.item
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
self = self.sudo()
|
||||||
|
if self.state == "open":
|
||||||
|
if self.line_ids:
|
||||||
|
# 1
|
||||||
|
all_variables_dict = {}
|
||||||
|
all_kpis = self.line_ids.mapped("kpi_id")
|
||||||
|
all_measures, all_constants, all_other_kpis = all_kpis._get_measures()
|
||||||
|
all_kpis += all_other_kpis
|
||||||
|
for measure in all_measures:
|
||||||
|
value = measure._calculate_for_period(self)
|
||||||
|
all_variables_dict.update({"MEASURE({})".format(measure.id): value})
|
||||||
|
for cosntant in all_constants:
|
||||||
|
value = cosntant._get_value_by_period(self)
|
||||||
|
all_variables_dict.update({"CONST({})".format(cosntant.id): value})
|
||||||
|
all_variables_dict.update({
|
||||||
|
"PERIOD_len": self.period_length,
|
||||||
|
"PERIOD_passed": self.period_passed,
|
||||||
|
})
|
||||||
|
# 2
|
||||||
|
lines_iterator = 0
|
||||||
|
while all_kpis:
|
||||||
|
this_kpi = all_kpis[lines_iterator]
|
||||||
|
kpi_id_reference = "KPI({})".format(this_kpi.id)
|
||||||
|
if not all_variables_dict.get(kpi_id_reference):
|
||||||
|
other_kpis = this_kpi.kpi_ids
|
||||||
|
for other_kpi in other_kpis:
|
||||||
|
other_value = all_variables_dict.get("KPI({})".format(other_kpi.id))
|
||||||
|
if other_value is None:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# we have all data to process formula
|
||||||
|
all_kpis -= this_kpi
|
||||||
|
actual_value = this_kpi._calculate_by_measure(all_variables_dict)
|
||||||
|
all_variables_dict.update({kpi_id_reference: actual_value})
|
||||||
|
else:
|
||||||
|
# seems excess to be here; but left for sudden cases
|
||||||
|
all_kpis -= this_kpi
|
||||||
|
all_variables_dict.update({kpi_id_reference: all_variables_dict.get(kpi_id_reference)})
|
||||||
|
lines_iterator += 1
|
||||||
|
if lines_iterator >= len(all_kpis):
|
||||||
|
# start from missed factors
|
||||||
|
lines_iterator = 0
|
||||||
|
# 3
|
||||||
|
for line in self.line_ids:
|
||||||
|
kpi_id_reference = "KPI({})".format(line.kpi_id.id)
|
||||||
|
actual_value = all_variables_dict.get(kpi_id_reference)
|
||||||
|
if isinstance(actual_value, str):
|
||||||
|
# it is the error
|
||||||
|
line.write({
|
||||||
|
"computation_error": actual_value,
|
||||||
|
"actual_value": 0,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
line.write({
|
||||||
|
"computation_error": False,
|
||||||
|
"actual_value": actual_value,
|
||||||
|
})
|
||||||
|
self.last_recalculation_date = fields.Datetime.now()
|
||||||
|
self._cr.commit()
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
from odoo import _, fields, models
|
||||||
|
|
||||||
|
class kpi_period_value(models.Model):
|
||||||
|
"""
|
||||||
|
The model to distinguish KPIs constant values by periods
|
||||||
|
"""
|
||||||
|
_name = "kpi.period.value"
|
||||||
|
_description = "KPI Period Value"
|
||||||
|
_rec_name = "period_id"
|
||||||
|
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
"kpi.period",
|
||||||
|
string="Period",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
constant_id = fields.Many2one(
|
||||||
|
"kpi.constant",
|
||||||
|
string="KPI Constant",
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
target_value = fields.Float(
|
||||||
|
string="Target Value",
|
||||||
|
)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
'period_constant_id_uniq',
|
||||||
|
'unique(period_id,constant_id)',
|
||||||
|
_('Period should be unique per each constant!'),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,341 @@
|
||||||
|
#coding: utf-8
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class kpi_scorecard_line(models.Model):
|
||||||
|
"""
|
||||||
|
KPI target
|
||||||
|
"""
|
||||||
|
_name = "kpi.scorecard.line"
|
||||||
|
_inherit = "kpi.help"
|
||||||
|
_description = "KPI Target"
|
||||||
|
_rec_name = "kpi_id"
|
||||||
|
|
||||||
|
@api.depends("target_value", "actual_value", "computation_error", "kpi_id", "kpi_id.result_appearance",
|
||||||
|
"kpi_id.result_preffix", "kpi_id.result_suffix", "kpi_id.result_type")
|
||||||
|
def _compute_formatted_actual_value(self):
|
||||||
|
"""
|
||||||
|
Compute method for formatted_actual_value, result, formatted_target_value
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _return_formated_appearance of kpi.item
|
||||||
|
"""
|
||||||
|
for line in self:
|
||||||
|
kpi_id = line.kpi_id
|
||||||
|
company_currency = line.period_id.company_id.currency_id
|
||||||
|
if line.computation_error:
|
||||||
|
line.formatted_actual_value = _("N/A")
|
||||||
|
line.result = "error"
|
||||||
|
line.formatted_target_value = kpi_id._return_formated_appearance(
|
||||||
|
line.target_value, novalue_change=True, currency=company_currency,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
actual_value = line.actual_value
|
||||||
|
line.formatted_actual_value = kpi_id._return_formated_appearance(
|
||||||
|
line.actual_value, novalue_change=False, currency=company_currency,
|
||||||
|
)
|
||||||
|
line.formatted_target_value = kpi_id._return_formated_appearance(
|
||||||
|
line.target_value, novalue_change=True, currency=company_currency,
|
||||||
|
)
|
||||||
|
result_type = kpi_id.result_type
|
||||||
|
actual_value = line.kpi_id.result_appearance == "percentage" and (actual_value * 100) or actual_value
|
||||||
|
bigger_result = actual_value >= line.target_value
|
||||||
|
if result_type == "more":
|
||||||
|
line.result = bigger_result and "success" or "failure"
|
||||||
|
elif result_type == "less":
|
||||||
|
line.result = bigger_result and "failure" or "success"
|
||||||
|
|
||||||
|
@api.depends("period_id", "period_id.line_ids", "period_id.line_ids.kpi_id", "period_id.line_ids.kpi_id.sequence",
|
||||||
|
"period_id.line_ids.kpi_id.all_parent_ids")
|
||||||
|
def _compute_sequence(self):
|
||||||
|
"""
|
||||||
|
The method to re-compute sequence, parent_id for lines
|
||||||
|
"""
|
||||||
|
periods = self.mapped("period_id")
|
||||||
|
for period in periods:
|
||||||
|
res_hierarchy = period.line_ids.action_get_hierarchy()
|
||||||
|
cur_sequence = 0
|
||||||
|
for line_dict in res_hierarchy:
|
||||||
|
line = line_dict.get("line")
|
||||||
|
line.sequence = cur_sequence
|
||||||
|
parent = line_dict.get("parent")
|
||||||
|
line.all_parents = parent \
|
||||||
|
and "{}{}".format(
|
||||||
|
parent.id,
|
||||||
|
parent.all_parents and "{}".format("," + parent.all_parents) or ""
|
||||||
|
) \
|
||||||
|
or False
|
||||||
|
cur_sequence += 1
|
||||||
|
|
||||||
|
@api.depends("kpi_id", "kpi_id.edit_access_user_ids")
|
||||||
|
def _compute_edit_rights(self):
|
||||||
|
"""
|
||||||
|
Compute method for edit_rights
|
||||||
|
"""
|
||||||
|
active_user = self.env.user
|
||||||
|
admin_rights = active_user.has_group("kpi_scorecard.group_kpi_admin")
|
||||||
|
for line in self:
|
||||||
|
line.edit_rights = admin_rights or active_user in line.kpi_id.edit_access_user_ids or False
|
||||||
|
|
||||||
|
kpi_id = fields.Many2one(
|
||||||
|
"kpi.item",
|
||||||
|
string="KPI",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
category_id = fields.Many2one(
|
||||||
|
"kpi.category",
|
||||||
|
related="kpi_id.category_id",
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
description = fields.Text(
|
||||||
|
related="kpi_id.description",
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
"kpi.period",
|
||||||
|
string="Period",
|
||||||
|
ondelete="cascade",
|
||||||
|
)
|
||||||
|
target_value = fields.Float(string="Target Value")
|
||||||
|
formatted_target_value = fields.Char(
|
||||||
|
string="Target",
|
||||||
|
compute=_compute_formatted_actual_value,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
actual_value = fields.Float(string="Actual Value")
|
||||||
|
computation_error = fields.Char(string="Logs")
|
||||||
|
formatted_actual_value = fields.Char(
|
||||||
|
string="Actual",
|
||||||
|
compute=_compute_formatted_actual_value,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
result = fields.Selection(
|
||||||
|
[
|
||||||
|
("success", "Success"),
|
||||||
|
("failure", "Failure"),
|
||||||
|
("error", "Error"),
|
||||||
|
],
|
||||||
|
compute=_compute_formatted_actual_value,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
sequence = fields.Integer(
|
||||||
|
string="Sequence",
|
||||||
|
compute=_compute_sequence,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
all_parents = fields.Char(
|
||||||
|
string="All Parents",
|
||||||
|
compute=_compute_sequence,
|
||||||
|
compute_sudo=True,
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
edit_rights = fields.Boolean(
|
||||||
|
"Active User Editor",
|
||||||
|
compute=_compute_edit_rights,
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
related="period_id.company_id",
|
||||||
|
store=True,
|
||||||
|
compute_sudo=True,
|
||||||
|
related_sudo=True,
|
||||||
|
)
|
||||||
|
period_length = fields.Float(
|
||||||
|
related="period_id.period_length",
|
||||||
|
store=True,
|
||||||
|
compute_sudo=True,
|
||||||
|
related_sudo=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_order = "sequence, id"
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
'period_kpi_uniq',
|
||||||
|
'unique(period_id, kpi_id)',
|
||||||
|
_('Target for this KPI is already set for this period'),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
"""
|
||||||
|
Overloading the method to make a name, since it doesn't have own
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for line in self:
|
||||||
|
name = "{} ({})".format(line.kpi_id.name, line.period_id.name_get()[0][1])
|
||||||
|
result.append((line.id, name))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def action_get_hierarchy(self):
|
||||||
|
"""
|
||||||
|
The method to get hirarchy of KPI targets
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get_levels_recursively
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* list of dict
|
||||||
|
"""
|
||||||
|
kpi_ids = self.mapped("kpi_id")
|
||||||
|
result = []
|
||||||
|
if kpi_ids:
|
||||||
|
kpi_ids = kpi_ids.sorted("sequence")
|
||||||
|
kpi_lines_dict = {line.kpi_id.id: line for line in self}
|
||||||
|
parent_kpis = kpi_ids.filtered(lambda kpi: not kpi.parent_id or kpi.parent_id.id not in kpi_ids.ids)
|
||||||
|
for parent_kpi in parent_kpis:
|
||||||
|
line_id = kpi_lines_dict.get(parent_kpi.id)
|
||||||
|
result += line_id._get_levels_recursively(parent_kpi, kpi_ids.ids, kpi_lines_dict, False)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def action_get_history(self):
|
||||||
|
"""
|
||||||
|
The method to find targets for the same KPI, same company and similar periods
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* dict :
|
||||||
|
** all_similar_targets - list of dicts to show in the table with formatted values
|
||||||
|
** similar_targets - list of dicts for graph
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
company_id = self.company_id
|
||||||
|
all_similar_targets = self.search([
|
||||||
|
("kpi_id", "=", self.kpi_id.id),
|
||||||
|
("company_id", "=", company_id.id),
|
||||||
|
], order="period_id")
|
||||||
|
all_similar_targets = all_similar_targets.mapped(lambda target: {
|
||||||
|
"period_id": target.period_id.name_get()[0][1],
|
||||||
|
"formatted_target_value": target.formatted_target_value,
|
||||||
|
"formatted_actual_value": target.formatted_actual_value,
|
||||||
|
"result": target.result,
|
||||||
|
})
|
||||||
|
lang = self._context.get("lang")
|
||||||
|
lang_date_format = "%m/%d/%Y"
|
||||||
|
if lang:
|
||||||
|
record_lang = self.env['res.lang'].search([("code", "=", lang)], limit=1)
|
||||||
|
lang_date_format = record_lang.date_format
|
||||||
|
similar_targets = self.search([
|
||||||
|
("kpi_id", "=", self.kpi_id.id),
|
||||||
|
("company_id", "=", company_id.id),
|
||||||
|
("period_length", ">=", self.period_length - company_id.kpi_history_tolerance),
|
||||||
|
("period_length", "<=", self.period_length + company_id.kpi_history_tolerance),
|
||||||
|
], order="period_id DESC")
|
||||||
|
similar_targets = similar_targets.mapped(lambda target: {
|
||||||
|
"date": target.period_id.date_end.strftime(lang_date_format),
|
||||||
|
"value": not target.computation_error and target.kpi_id._return_rounded_value(target.actual_value, False) \
|
||||||
|
or 0,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"all_similar_targets": all_similar_targets,
|
||||||
|
"similar_targets": similar_targets,
|
||||||
|
"kpi_name": self.kpi_id.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_levels_recursively(self, parent_kpi, all_kpis, kpi_lines_dict, parent_line=False,):
|
||||||
|
"""
|
||||||
|
The recursion method to get child kpi one by one
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* parent_kpi - kpi.item record
|
||||||
|
* all_kpis - list of ints
|
||||||
|
* kpi_lines_dict - dict of relations kpi.scorecard.line (id - int) - kpi.item (id - int)
|
||||||
|
* parent_line - id of parent
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _get_value_dict
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* list of dicts
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
result = [self._get_value_dict(all_kpis, parent_line)]
|
||||||
|
child_ids = self.kpi_id.child_ids
|
||||||
|
for child_kpi in child_ids:
|
||||||
|
child_kpi_id = child_kpi.id
|
||||||
|
if child_kpi_id in all_kpis:
|
||||||
|
line_id = kpi_lines_dict.get(child_kpi_id)
|
||||||
|
result += line_id._get_levels_recursively(child_kpi, all_kpis, kpi_lines_dict, self)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_value_dict(self, all_kpis, parent_line=False):
|
||||||
|
"""
|
||||||
|
The method to prepare values dict of js
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* all_kpis - list of ints
|
||||||
|
* parent_line - kpi.scorecard.line -parent
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* list of dict
|
||||||
|
** line - kpi.scorecard.line
|
||||||
|
** parent - int of parent if exist, False otherwise
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"line": self,
|
||||||
|
"parent": parent_line,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_xls_table(self):
|
||||||
|
"""
|
||||||
|
The method to prepare dict of values for xls row
|
||||||
|
|
||||||
|
Args:
|
||||||
|
* spaces - str - to add at the beginning of the name
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* _return_xls_formatting - of kpi.item
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* dict
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
row = 2
|
||||||
|
previous_kpis = {}
|
||||||
|
for line in self:
|
||||||
|
parent_id = line.kpi_id.parent_id.id
|
||||||
|
level = previous_kpis.get(parent_id) is not None and previous_kpis.get(parent_id) + 1 or 0
|
||||||
|
previous_kpis.update({line.kpi_id.id: level})
|
||||||
|
description = line.description or ""
|
||||||
|
target_value = line.target_value
|
||||||
|
actual_value = line.actual_value
|
||||||
|
overall_style = {
|
||||||
|
"color": line.result == "success" and "black" or line.result == "failure" and "red" or "orange"
|
||||||
|
}
|
||||||
|
if line.computation_error:
|
||||||
|
target_value = 0
|
||||||
|
actual_value = 0
|
||||||
|
description = "{} {}".format(line.computation_error, description)
|
||||||
|
overall_style.update({"color": "orange"})
|
||||||
|
target_value = line.kpi_id._return_xls_formatting(line.target_value, False)
|
||||||
|
actual_value = line.kpi_id._return_xls_formatting(line.actual_value, True)
|
||||||
|
num_style = overall_style.copy()
|
||||||
|
num_style.update({
|
||||||
|
"align": "center",
|
||||||
|
})
|
||||||
|
if line.kpi_id.result_appearance == "percentage":
|
||||||
|
num_style.update({"num_format": 10}),
|
||||||
|
overall_style.update({
|
||||||
|
"valign": "vjustify",
|
||||||
|
})
|
||||||
|
result.update({
|
||||||
|
"A{}".format(row): {"value": line.kpi_id.name, "style": overall_style, "level": level},
|
||||||
|
"B{}".format(row): {"value": target_value, "style": num_style},
|
||||||
|
"C{}".format(row): {"value": actual_value, "style": num_style},
|
||||||
|
"D{}".format(row): {"value": description, "style": overall_style},
|
||||||
|
})
|
||||||
|
row += 1
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class res_company(models.Model):
|
||||||
|
"""
|
||||||
|
Overwrite to keep settings in company
|
||||||
|
"""
|
||||||
|
_inherit = "res.company"
|
||||||
|
|
||||||
|
kpi_history_tolerance = fields.Integer(
|
||||||
|
string="History Tolerance",
|
||||||
|
default=3,
|
||||||
|
)
|
||||||
|
show_kpi_help = fields.Boolean(
|
||||||
|
string="Show Help Tabs",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import _, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class res_config_settings(models.TransientModel):
|
||||||
|
"""
|
||||||
|
The model to keep settings of business appointments on website
|
||||||
|
"""
|
||||||
|
_inherit = "res.config.settings"
|
||||||
|
|
||||||
|
kpi_company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
string="Company for KPI settings",
|
||||||
|
default=lambda self: self.env.user.company_id,
|
||||||
|
)
|
||||||
|
kpi_history_tolerance = fields.Integer(
|
||||||
|
related="kpi_company_id.kpi_history_tolerance",
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
show_kpi_help = fields.Boolean(
|
||||||
|
related="kpi_company_id.show_kpi_help",
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_open_kpi_cron(self):
|
||||||
|
"""
|
||||||
|
The method to open ir.cron of kpi update
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* action dict
|
||||||
|
|
||||||
|
Extra info:
|
||||||
|
* Expected singleton
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
cron_id = self.sudo().env.ref("kpi_scorecard.cron_recalculate_kpi_periods", False)
|
||||||
|
if cron_id:
|
||||||
|
return {
|
||||||
|
"res_id": cron_id.id,
|
||||||
|
"name": _("Job: Calculate KPIs"),
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "ir.cron",
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "new",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_kpi_measure,access_kpi_measure,model_kpi_measure,kpi_scorecard.group_kpi_user,1,0,0,0
|
||||||
|
access_kpi_measure_admin,access_kpi_measure_admin,model_kpi_measure,kpi_scorecard.group_kpi_admin,1,1,1,1
|
||||||
|
access_kpi_constant,access_kpi_constant,model_kpi_constant,kpi_scorecard.group_kpi_user,1,0,0,0
|
||||||
|
access_kpi_constant_admin,access_kpi_constant_admin,model_kpi_constant,kpi_scorecard.group_kpi_admin,1,1,1,1
|
||||||
|
access_kpi_period_value,access_kpi_period_value,model_kpi_period_value,kpi_scorecard.group_kpi_user,1,0,0,0
|
||||||
|
access_kpi_period_value_admin,access_kpi_period_value_admin,model_kpi_period_value,kpi_scorecard.group_kpi_admin,1,1,1,1
|
||||||
|
access_kpi_measure_item,access_kpi_measure_item,model_kpi_measure_item,kpi_scorecard.group_kpi_user,1,0,0,0
|
||||||
|
access_kpi_measure_item_admin,access_kpi_measure_item_admin,model_kpi_measure_item,kpi_scorecard.group_kpi_admin,1,1,1,1
|
||||||
|
access_kpi_category,access_kpi_category,model_kpi_category,kpi_scorecard.group_kpi_user,1,0,0,0
|
||||||
|
access_kpi_category_admin,access_kpi_category_admin,model_kpi_category,kpi_scorecard.group_kpi_admin,1,1,1,1
|
||||||
|
access_kpi_item,access_kpi_item,model_kpi_item,kpi_scorecard.group_kpi_user,1,0,0,0
|
||||||
|
access_kpi_item_admin,access_kpi_item_admin,model_kpi_item,kpi_scorecard.group_kpi_admin,1,1,1,1
|
||||||
|
access_kpi_period,access_kpi_period,model_kpi_period,kpi_scorecard.group_kpi_user,1,0,0,0
|
||||||
|
access_kpi_period_admin,access_kpi_period_admin,model_kpi_period,kpi_scorecard.group_kpi_admin,1,1,1,1
|
||||||
|
access_kpi_scorecard_line,access_kpi_scorecard_line,model_kpi_scorecard_line,kpi_scorecard.group_kpi_user,1,1,1,1
|
||||||
|
access_kpi_copy_template,access_kpi_copy_template,model_kpi_copy_template,kpi_scorecard.group_kpi_admin,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="module_category_kpi" model="ir.module.category">
|
||||||
|
<field name="name">KPI Management</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_kpi_user" model="res.groups">
|
||||||
|
<field name="name">KPI User</field>
|
||||||
|
<field name="category_id" ref="kpi_scorecard.module_category_kpi"/>
|
||||||
|
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_kpi_admin" model="res.groups">
|
||||||
|
<field name="name">KPI Manager</field>
|
||||||
|
<field name="category_id" ref="kpi_scorecard.module_category_kpi"/>
|
||||||
|
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<data noupdate="0">
|
||||||
|
<!-- KPI Constants -->
|
||||||
|
<record id="kpi_constant_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">KPI Constants: Multi Company</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_constant"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
('company_id','=', False),
|
||||||
|
('company_id', 'in', company_ids),
|
||||||
|
]</field>
|
||||||
|
</record>
|
||||||
|
<!-- KPI Measurements -->
|
||||||
|
<record id="kpi_measure_item_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">KPI Measurements (Variables): Multi Company</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_measure_item"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
('company_id','=', False),
|
||||||
|
('company_id', 'in', company_ids),
|
||||||
|
]</field>
|
||||||
|
</record>
|
||||||
|
<!-- KPI Periods -->
|
||||||
|
<record id="kpi_period_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">KPI Periods: Multi Company</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_period"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
('company_id','=', False),
|
||||||
|
('company_id', 'in', company_ids),
|
||||||
|
]</field>
|
||||||
|
</record>
|
||||||
|
<!-- KPI Categories -->
|
||||||
|
<record id="kpi_category_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">All users - all KPI Categories of own company</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_category"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
("access_user_ids", "in", [user.id]),
|
||||||
|
("edit_access_user_ids", "in", [user.id]),
|
||||||
|
'|',
|
||||||
|
('company_id','=', False),
|
||||||
|
('company_id', 'in', company_ids),
|
||||||
|
]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="kpi_category_admin_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">KPI Admin - all KPI Categories of own company</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_category"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
('company_id','=', False),
|
||||||
|
('company_id', 'in', company_ids),
|
||||||
|
]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('kpi_scorecard.group_kpi_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
<!-- KPIs -->
|
||||||
|
<record id="kpi_item_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">All users: KPIs ow own companies</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_item"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
("access_user_ids", "in", [user.id]),
|
||||||
|
("edit_access_user_ids", "in", [user.id]),
|
||||||
|
'|',
|
||||||
|
('company_id','=', user.company_id.id),
|
||||||
|
('company_id','=', False),
|
||||||
|
]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="kpi_item__admin_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">KPIs: Multi Company</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_item"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
('company_id','=', False),
|
||||||
|
('company_id', 'in', company_ids),
|
||||||
|
]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('kpi_scorecard.group_kpi_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
<!-- KPI Targets -->
|
||||||
|
<record id="kpi_scorecard_line_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">All users - Read targets of own companies related to them / their groups</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_scorecard_line"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
("kpi_id.access_user_ids", "in", [user.id]),
|
||||||
|
("kpi_id.edit_access_user_ids", "in", [user.id]),
|
||||||
|
'|',
|
||||||
|
('period_id.company_id', '=', False),
|
||||||
|
('period_id.company_id', '=', user.company_id.id),
|
||||||
|
]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="False"/>
|
||||||
|
<field name="perm_create" eval="False"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
<record id="kpi_scorecard_line_edit_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">All users - Edit targets of own companies related to them / their groups</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_scorecard_line"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
("kpi_id.edit_access_user_ids", "in", [user.id]),
|
||||||
|
'|',
|
||||||
|
('period_id.company_id', '=', False),
|
||||||
|
('period_id.company_id', 'in', company_ids),
|
||||||
|
]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="perm_read" eval="False"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<record id="kpi_scorecard_line_admin_multi_company_rule" model="ir.rule">
|
||||||
|
<field name="name">KPI Admin - any KPI Targets of own companies</field>
|
||||||
|
<field name="model_id" ref="kpi_scorecard.model_kpi_scorecard_line"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
('period_id.company_id', '=', False),
|
||||||
|
('period_id.company_id', 'in', company_ids),
|
||||||
|
]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('kpi_scorecard.group_kpi_admin'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
||||||