init common and customize

This commit is contained in:
Ivan Office
2024-08-19 18:41:50 +08:00
parent 6f068e6c27
commit 7025bb9611
354 changed files with 14557 additions and 0 deletions

7
app_common/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from .hooks import pre_init_hook
from .hooks import post_init_hook
from .hooks import uninstall_hook

View File

@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# Created on 2023-02-02
# author: 欧度智能https://www.odooai.cn
# email: 300883@qq.com
# resource of odooai
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
# Odoo16在线用户手册长期更新
# https://www.odooai.cn/documentation/16.0/zh_CN/index.html
# Odoo16在线开发者手册长期更新
# https://www.odooai.cn/documentation/16.0/zh_CN/developer.html
# Odoo13在线用户手册长期更新
# https://www.odooai.cn/documentation/user/13.0/zh_CN/index.html
# Odoo13在线开发者手册长期更新
# https://www.odooai.cn/documentation/13.0/index.html
# Odoo10在线中文用户手册长期更新
# https://www.odooai.cn/documentation/user/10.0/zh_CN/index.html
# Odoo10离线中文用户手册下载
# https://www.odooai.cn/odoo10_user_manual_document_offline/
# Odoo10离线开发手册下载-含python教程jquery参考Jinja2模板PostgresSQL参考odoo开发必备
# https://www.odooai.cn/odoo10_developer_document_offline/
##############################################################################
# Copyright (C) 2009-TODAY odooai.cn Ltd. https://www.odooai.cn
# Author: Ivan Deng300883@qq.com
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
# See <http://www.gnu.org/licenses/>.
#
# It is forbidden to publish, distribute, sublicense, or sell copies
# of the Software or modified copies of the Software.
##############################################################################
{
'name': "odooAi Common Util and Tools",
'version': '24.08.19',
'author': 'odooai.cn',
'category': 'Base',
'website': 'https://www.odooai.cn',
'live_test_url': 'https://demo.odooapp.cn',
'license': 'LGPL-3',
'sequence': 2,
'price': 0.00,
'currency': 'EUR',
'images': ['static/description/banner.png'],
'summary': '''
Core for common use for odooai.cn apps.
基础核心必须没有要被依赖字段及视图等实现auto_install
''',
'description': '''
need to setup odoo.conf, add follow:
server_wide_modules = web,app_common
1. Quick import data from excel with .py code
2. Quick m2o default value
3. Filter for useless field
4. UTC local timezone convert
5. Get browser ua, user-agent
6. Image to local, image url to local, media to local attachment
7. Log cron job
8. Boost for less no use mail
9. Customize .rng file
10. Misc like get distance between two points
11. Multi-language Support. Multi-Company Support.
12. Support Odoo 17,16,15,14,13,12, Enterprise and Community and odoo.sh Edition.
13. Full Open Source.
==========
1.
2.
3. 多语言支持
4. 多公司支持
5. Odoo 16, 企业版,社区版,多版本支持
''',
'depends': [
'mail',
'web',
],
'data': [
'views/ir_cron_views.xml',
# 'report/.xml',
],
'qweb': [
'static/src/xml/*.xml',
],
'demo': [],
# 'pre_init_hook': 'pre_init_hook',
# 'post_init_hook': 'post_init_hook',
# 'uninstall_hook': 'uninstall_hook',
# 可以不需要,因为直接放 common中了
# 'external_dependencies': {'python': ['pyyaml', 'ua-parser', 'user-agents']},
'installable': True,
'application': True,
'auto_install': True,
}

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import main

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
import base64
from io import BytesIO
import requests
from math import radians, cos, sin, asin, sqrt
from ..lib.user_agents import parse
from ..models.base import get_ua_type
from odoo import api, http, SUPERUSER_ID, _
from odoo import http, exceptions
from odoo.http import request
import logging
_logger = logging.getLogger(__name__)
class AppController(http.Controller):
def get_image_from_url(self, url):
if not url:
return None
try:
response = requests.get(url) # 将这个图片保存在内存
except Exception as e:
return None
# 返回这个图片的base64编码
return base64.b64encode(BytesIO(response.content).read())
@http.route(['/my/ua', '/wxa/ua', '/web/ua', '/web/ua/show'], auth='public', methods=['GET'])
def app_ua_show(self):
# https://github.com/selwin/python-user-agents
ua_string = request.httprequest.headers.get('User-Agent')
user_agent = parse(ua_string)
ua_type = get_ua_type()
ustr = "Request UA: <br/> %s <br/>Parse UA: <br/>%s <br/>UA Type:<br/>%s <br/>" % (ua_string, str(user_agent), ua_type)
return request.make_response(ustr, [('Content-Type', 'text/html')])
def get_ua_type(self):
return get_ua_type()
def haversine(lon1, lat1, lon2, lat2):
# 计算地图上两点的距离
# in:经度1纬度1经度2纬度2 (十进制度数)
# out: 距离(米)
"""
Calculate the great circle distance between two points
on the earth (specified in decimal degrees)
"""
# 将十进制度数转化为弧度
lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
# haversine公式
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
c = 2 * asin(sqrt(a))
r = 6371 # 地球平均半径,单位为公里
return c * r * 1000

32
app_common/hooks.py Normal file
View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Created on 2018-10-12
# author: 欧度智能https://www.odooai.cn
# email: 300883@qq.com
# resource of odooai
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
# Odoo在线中文用户手册长期更新
# https://www.odooai.cn/documentation/user/10.0/zh_CN/index.html
# Odoo10离线中文用户手册下载
# https://www.odooai.cn/odoo10_user_manual_document_offline/
# Odoo10离线开发手册下载-含python教程jquery参考Jinja2模板PostgresSQL参考odoo开发必备
# https://www.odooai.cn/odoo10_developer_document_offline/
# description:
from odoo import api, SUPERUSER_ID, _
def pre_init_hook(env):
pass
# cr.execute("")
def post_init_hook(env):
pass
# cr.execute("")
def uninstall_hook(env):
pass
# cr.execute("")

29
app_common/i18n/zh_CN.po Normal file
View File

@@ -0,0 +1,29 @@
# Translation of Odoo Server.
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-01-08 14:28+0000\n"
"PO-Revision-Date: 2018-01-08 14:28+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: app_common
#: model:ir.model.fields,field_description:app_common.field_ir_cron__trigger_user_id
msgid "Last Trigger User"
msgstr "手动运行用户"
#. module: app_common
#: model:ir.model,name:app_common.model_ir_cron
msgid "Scheduled Actions"
msgstr "安排的动作"
#. module: app_common
#: model:ir.model,name:app_common.model_ir_ui_view
msgid "View"
msgstr "查看"

View File

@@ -0,0 +1 @@
VERSION = (0, 10, 0)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,544 @@
# Copyright 2009 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the 'License')
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Python implementation of the UA parser."""
from __future__ import absolute_import
import os
import re
__author__ = "Lindsey Simon <elsigh@gmail.com>"
class UserAgentParser(object):
def __init__(
self, pattern, family_replacement=None, v1_replacement=None, v2_replacement=None
):
"""Initialize UserAgentParser.
Args:
pattern: a regular expression string
family_replacement: a string to override the matched family (optional)
v1_replacement: a string to override the matched v1 (optional)
v2_replacement: a string to override the matched v2 (optional)
"""
self.pattern = pattern
self.user_agent_re = re.compile(self.pattern)
self.family_replacement = family_replacement
self.v1_replacement = v1_replacement
self.v2_replacement = v2_replacement
def MatchSpans(self, user_agent_string):
match_spans = []
match = self.user_agent_re.search(user_agent_string)
if match:
match_spans = [
match.span(group_index) for group_index in range(1, match.lastindex + 1)
]
return match_spans
def Parse(self, user_agent_string):
family, v1, v2, v3 = None, None, None, None
match = self.user_agent_re.search(user_agent_string)
if match:
if self.family_replacement:
if re.search(r"\$1", self.family_replacement):
family = re.sub(r"\$1", match.group(1), self.family_replacement)
else:
family = self.family_replacement
else:
family = match.group(1)
if self.v1_replacement:
v1 = self.v1_replacement
elif match.lastindex and match.lastindex >= 2:
v1 = match.group(2) or None
if self.v2_replacement:
v2 = self.v2_replacement
elif match.lastindex and match.lastindex >= 3:
v2 = match.group(3) or None
if match.lastindex and match.lastindex >= 4:
v3 = match.group(4) or None
return family, v1, v2, v3
class OSParser(object):
def __init__(
self,
pattern,
os_replacement=None,
os_v1_replacement=None,
os_v2_replacement=None,
os_v3_replacement=None,
os_v4_replacement=None,
):
"""Initialize UserAgentParser.
Args:
pattern: a regular expression string
os_replacement: a string to override the matched os (optional)
os_v1_replacement: a string to override the matched v1 (optional)
os_v2_replacement: a string to override the matched v2 (optional)
os_v3_replacement: a string to override the matched v3 (optional)
os_v4_replacement: a string to override the matched v4 (optional)
"""
self.pattern = pattern
self.user_agent_re = re.compile(self.pattern)
self.os_replacement = os_replacement
self.os_v1_replacement = os_v1_replacement
self.os_v2_replacement = os_v2_replacement
self.os_v3_replacement = os_v3_replacement
self.os_v4_replacement = os_v4_replacement
def MatchSpans(self, user_agent_string):
match_spans = []
match = self.user_agent_re.search(user_agent_string)
if match:
match_spans = [
match.span(group_index) for group_index in range(1, match.lastindex + 1)
]
return match_spans
def Parse(self, user_agent_string):
os, os_v1, os_v2, os_v3, os_v4 = None, None, None, None, None
match = self.user_agent_re.search(user_agent_string)
if match:
if self.os_replacement:
os = MultiReplace(self.os_replacement, match)
elif match.lastindex:
os = match.group(1)
if self.os_v1_replacement:
os_v1 = MultiReplace(self.os_v1_replacement, match)
elif match.lastindex and match.lastindex >= 2:
os_v1 = match.group(2)
if self.os_v2_replacement:
os_v2 = MultiReplace(self.os_v2_replacement, match)
elif match.lastindex and match.lastindex >= 3:
os_v2 = match.group(3)
if self.os_v3_replacement:
os_v3 = MultiReplace(self.os_v3_replacement, match)
elif match.lastindex and match.lastindex >= 4:
os_v3 = match.group(4)
if self.os_v4_replacement:
os_v4 = MultiReplace(self.os_v4_replacement, match)
elif match.lastindex and match.lastindex >= 5:
os_v4 = match.group(5)
return os, os_v1, os_v2, os_v3, os_v4
def MultiReplace(string, match):
def _repl(m):
index = int(m.group(1)) - 1
group = match.groups()
if index < len(group):
return group[index]
return ""
_string = re.sub(r"\$(\d)", _repl, string)
_string = re.sub(r"^\s+|\s+$", "", _string)
if _string == "":
return None
return _string
class DeviceParser(object):
def __init__(
self,
pattern,
regex_flag=None,
device_replacement=None,
brand_replacement=None,
model_replacement=None,
):
"""Initialize UserAgentParser.
Args:
pattern: a regular expression string
device_replacement: a string to override the matched device (optional)
"""
self.pattern = pattern
if regex_flag == "i":
self.user_agent_re = re.compile(self.pattern, re.IGNORECASE)
else:
self.user_agent_re = re.compile(self.pattern)
self.device_replacement = device_replacement
self.brand_replacement = brand_replacement
self.model_replacement = model_replacement
def MatchSpans(self, user_agent_string):
match_spans = []
match = self.user_agent_re.search(user_agent_string)
if match:
match_spans = [
match.span(group_index) for group_index in range(1, match.lastindex + 1)
]
return match_spans
def Parse(self, user_agent_string):
device, brand, model = None, None, None
match = self.user_agent_re.search(user_agent_string)
if match:
if self.device_replacement:
device = MultiReplace(self.device_replacement, match)
else:
device = match.group(1)
if self.brand_replacement:
brand = MultiReplace(self.brand_replacement, match)
if self.model_replacement:
model = MultiReplace(self.model_replacement, match)
elif len(match.groups()) > 0:
model = match.group(1)
return device, brand, model
MAX_CACHE_SIZE = 20
_parse_cache = {}
def Parse(user_agent_string, **jsParseBits):
""" Parse all the things
Args:
user_agent_string: the full user agent string
jsParseBits: javascript override bits
Returns:
A dictionary containing all parsed bits
"""
jsParseBits = jsParseBits or {}
key = (user_agent_string, repr(jsParseBits))
cached = _parse_cache.get(key)
if cached is not None:
return cached
if len(_parse_cache) > MAX_CACHE_SIZE:
_parse_cache.clear()
v = {
"user_agent": ParseUserAgent(user_agent_string, **jsParseBits),
"os": ParseOS(user_agent_string, **jsParseBits),
"device": ParseDevice(user_agent_string, **jsParseBits),
"string": user_agent_string,
}
_parse_cache[key] = v
return v
def ParseUserAgent(user_agent_string, **jsParseBits):
""" Parses the user-agent string for user agent (browser) info.
Args:
user_agent_string: The full user-agent string.
jsParseBits: javascript override bits.
Returns:
A dictionary containing parsed bits.
"""
if (
"js_user_agent_family" in jsParseBits
and jsParseBits["js_user_agent_family"] != ""
):
family = jsParseBits["js_user_agent_family"]
v1 = jsParseBits.get("js_user_agent_v1") or None
v2 = jsParseBits.get("js_user_agent_v2") or None
v3 = jsParseBits.get("js_user_agent_v3") or None
else:
for uaParser in USER_AGENT_PARSERS:
family, v1, v2, v3 = uaParser.Parse(user_agent_string)
if family:
break
# Override for Chrome Frame IFF Chrome is enabled.
if "js_user_agent_string" in jsParseBits:
js_user_agent_string = jsParseBits["js_user_agent_string"]
if (
js_user_agent_string
and js_user_agent_string.find("Chrome/") > -1
and user_agent_string.find("chromeframe") > -1
):
jsOverride = {}
jsOverride = ParseUserAgent(js_user_agent_string)
family = "Chrome Frame (%s %s)" % (family, v1)
v1 = jsOverride["major"]
v2 = jsOverride["minor"]
v3 = jsOverride["patch"]
family = family or "Other"
return {
"family": family,
"major": v1 or None,
"minor": v2 or None,
"patch": v3 or None,
}
def ParseOS(user_agent_string, **jsParseBits):
""" Parses the user-agent string for operating system info
Args:
user_agent_string: The full user-agent string.
jsParseBits: javascript override bits.
Returns:
A dictionary containing parsed bits.
"""
for osParser in OS_PARSERS:
os, os_v1, os_v2, os_v3, os_v4 = osParser.Parse(user_agent_string)
if os:
break
os = os or "Other"
return {
"family": os,
"major": os_v1,
"minor": os_v2,
"patch": os_v3,
"patch_minor": os_v4,
}
def ParseDevice(user_agent_string):
""" Parses the user-agent string for device info.
Args:
user_agent_string: The full user-agent string.
ua_family: The parsed user agent family name.
Returns:
A dictionary containing parsed bits.
"""
for deviceParser in DEVICE_PARSERS:
device, brand, model = deviceParser.Parse(user_agent_string)
if device:
break
if device is None:
device = "Other"
return {"family": device, "brand": brand, "model": model}
def PrettyUserAgent(family, v1=None, v2=None, v3=None):
"""Pretty user agent string."""
if v3:
if v3[0].isdigit():
return "%s %s.%s.%s" % (family, v1, v2, v3)
else:
return "%s %s.%s%s" % (family, v1, v2, v3)
elif v2:
return "%s %s.%s" % (family, v1, v2)
elif v1:
return "%s %s" % (family, v1)
return family
def PrettyOS(os, os_v1=None, os_v2=None, os_v3=None, os_v4=None):
"""Pretty os string."""
if os_v4:
return "%s %s.%s.%s.%s" % (os, os_v1, os_v2, os_v3, os_v4)
if os_v3:
if os_v3[0].isdigit():
return "%s %s.%s.%s" % (os, os_v1, os_v2, os_v3)
else:
return "%s %s.%s%s" % (os, os_v1, os_v2, os_v3)
elif os_v2:
return "%s %s.%s" % (os, os_v1, os_v2)
elif os_v1:
return "%s %s" % (os, os_v1)
return os
def ParseWithJSOverrides(
user_agent_string,
js_user_agent_string=None,
js_user_agent_family=None,
js_user_agent_v1=None,
js_user_agent_v2=None,
js_user_agent_v3=None,
):
""" backwards compatible. use one of the other Parse methods instead! """
# Override via JS properties.
if js_user_agent_family is not None and js_user_agent_family != "":
family = js_user_agent_family
v1 = None
v2 = None
v3 = None
if js_user_agent_v1 is not None:
v1 = js_user_agent_v1
if js_user_agent_v2 is not None:
v2 = js_user_agent_v2
if js_user_agent_v3 is not None:
v3 = js_user_agent_v3
else:
for parser in USER_AGENT_PARSERS:
family, v1, v2, v3 = parser.Parse(user_agent_string)
if family:
break
# Override for Chrome Frame IFF Chrome is enabled.
if (
js_user_agent_string
and js_user_agent_string.find("Chrome/") > -1
and user_agent_string.find("chromeframe") > -1
):
family = "Chrome Frame (%s %s)" % (family, v1)
ua_dict = ParseUserAgent(js_user_agent_string)
v1 = ua_dict["major"]
v2 = ua_dict["minor"]
v3 = ua_dict["patch"]
return family or "Other", v1, v2, v3
def Pretty(family, v1=None, v2=None, v3=None):
""" backwards compatible. use PrettyUserAgent instead! """
if v3:
if v3[0].isdigit():
return "%s %s.%s.%s" % (family, v1, v2, v3)
else:
return "%s %s.%s%s" % (family, v1, v2, v3)
elif v2:
return "%s %s.%s" % (family, v1, v2)
elif v1:
return "%s %s" % (family, v1)
return family
def GetFilters(
user_agent_string,
js_user_agent_string=None,
js_user_agent_family=None,
js_user_agent_v1=None,
js_user_agent_v2=None,
js_user_agent_v3=None,
):
"""Return the optional arguments that should be saved and used to query.
js_user_agent_string is always returned if it is present. We really only need
it for Chrome Frame. However, I added it in the generally case to find other
cases when it is different. When the recording of js_user_agent_string was
added, we created new records for all new user agents.
Since we only added js_document_mode for the IE 9 preview case, it did not
cause new user agent records the way js_user_agent_string did.
js_document_mode has since been removed in favor of individual property
overrides.
Args:
user_agent_string: The full user-agent string.
js_user_agent_string: JavaScript ua string from client-side
js_user_agent_family: This is an override for the family name to deal
with the fact that IE platform preview (for instance) cannot be
distinguished by user_agent_string, but only in javascript.
js_user_agent_v1: v1 override - see above.
js_user_agent_v2: v1 override - see above.
js_user_agent_v3: v1 override - see above.
Returns:
{js_user_agent_string: '[...]', js_family_name: '[...]', etc...}
"""
filters = {}
filterdict = {
"js_user_agent_string": js_user_agent_string,
"js_user_agent_family": js_user_agent_family,
"js_user_agent_v1": js_user_agent_v1,
"js_user_agent_v2": js_user_agent_v2,
"js_user_agent_v3": js_user_agent_v3,
}
for key, value in filterdict.items():
if value is not None and value != "":
filters[key] = value
return filters
# Build the list of user agent parsers from YAML
UA_PARSER_YAML = os.environ.get("UA_PARSER_YAML")
if UA_PARSER_YAML:
# This will raise an ImportError if missing, obviously since it's no
# longer a requirement
import yaml
try:
# Try and use libyaml bindings if available since faster
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
with open(UA_PARSER_YAML) as fp:
regexes = yaml.load(fp, Loader=SafeLoader)
USER_AGENT_PARSERS = []
for _ua_parser in regexes["user_agent_parsers"]:
_regex = _ua_parser["regex"]
_family_replacement = _ua_parser.get("family_replacement")
_v1_replacement = _ua_parser.get("v1_replacement")
_v2_replacement = _ua_parser.get("v2_replacement")
USER_AGENT_PARSERS.append(
UserAgentParser(
_regex, _family_replacement, _v1_replacement, _v2_replacement
)
)
OS_PARSERS = []
for _os_parser in regexes["os_parsers"]:
_regex = _os_parser["regex"]
_os_replacement = _os_parser.get("os_replacement")
_os_v1_replacement = _os_parser.get("os_v1_replacement")
_os_v2_replacement = _os_parser.get("os_v2_replacement")
_os_v3_replacement = _os_parser.get("os_v3_replacement")
_os_v4_replacement = _os_parser.get("os_v4_replacement")
OS_PARSERS.append(
OSParser(
_regex,
_os_replacement,
_os_v1_replacement,
_os_v2_replacement,
_os_v3_replacement,
_os_v4_replacement,
)
)
DEVICE_PARSERS = []
for _device_parser in regexes["device_parsers"]:
_regex = _device_parser["regex"]
_regex_flag = _device_parser.get("regex_flag")
_device_replacement = _device_parser.get("device_replacement")
_brand_replacement = _device_parser.get("brand_replacement")
_model_replacement = _device_parser.get("model_replacement")
DEVICE_PARSERS.append(
DeviceParser(
_regex,
_regex_flag,
_device_replacement,
_brand_replacement,
_model_replacement,
)
)
# Clean our our temporary vars explicitly
# so they can't be reused or imported
del regexes
del yaml
del SafeLoader
else:
# Just load our pre-compiled versions
from ._regexes import USER_AGENT_PARSERS, DEVICE_PARSERS, OS_PARSERS

View File

@@ -0,0 +1,290 @@
#!/usr/bin/python2.5
#
# Copyright 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the 'License')
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""User Agent Parser Unit Tests.
Run:
# python -m user_agent_parser_test (runs all the tests, takes awhile)
or like:
# python -m user_agent_parser_test ParseTest.testBrowserscopeStrings
"""
from __future__ import unicode_literals, absolute_import
__author__ = "slamm@google.com (Stephen Lamm)"
import os
import re
import unittest
import yaml
try:
# Try and use libyaml bindings if available since faster
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from ua_parser import user_agent_parser
TEST_RESOURCES_DIR = os.path.join(
os.path.abspath(os.path.dirname(__file__)), "../uap-core"
)
class ParseTest(unittest.TestCase):
def testBrowserscopeStrings(self):
self.runUserAgentTestsFromYAML(
os.path.join(TEST_RESOURCES_DIR, "tests/test_ua.yaml")
)
def testBrowserscopeStringsOS(self):
self.runOSTestsFromYAML(os.path.join(TEST_RESOURCES_DIR, "tests/test_os.yaml"))
def testStringsOS(self):
self.runOSTestsFromYAML(
os.path.join(TEST_RESOURCES_DIR, "test_resources/additional_os_tests.yaml")
)
def testStringsDevice(self):
self.runDeviceTestsFromYAML(
os.path.join(TEST_RESOURCES_DIR, "tests/test_device.yaml")
)
def testMozillaStrings(self):
self.runUserAgentTestsFromYAML(
os.path.join(
TEST_RESOURCES_DIR, "test_resources/firefox_user_agent_strings.yaml"
)
)
# NOTE: The YAML file used here is one output by makePGTSComparisonYAML()
# below, as opposed to the pgts_browser_list-orig.yaml file. The -orig
# file is by no means perfect, but identifies many browsers that we
# classify as "Other". This test itself is mostly useful to know when
# somthing in UA parsing changes. An effort should be made to try and
# reconcile the differences between the two YAML files.
def testPGTSStrings(self):
self.runUserAgentTestsFromYAML(
os.path.join(TEST_RESOURCES_DIR, "test_resources/pgts_browser_list.yaml")
)
def testParseAll(self):
user_agent_string = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.4; fr; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5,gzip(gfe),gzip(gfe)"
expected = {
"device": {"family": "Mac", "brand": "Apple", "model": "Mac"},
"os": {
"family": "Mac OS X",
"major": "10",
"minor": "4",
"patch": None,
"patch_minor": None,
},
"user_agent": {
"family": "Firefox",
"major": "3",
"minor": "5",
"patch": "5",
},
"string": user_agent_string,
}
result = user_agent_parser.Parse(user_agent_string)
self.assertEqual(
result,
expected,
"UA: {0}\n expected<{1}> != actual<{2}>".format(
user_agent_string, expected, result
),
)
# Make a YAML file for manual comparsion with pgts_browser_list-orig.yaml
def makePGTSComparisonYAML(self):
import codecs
outfile = codecs.open("outfile.yaml", "w", "utf-8")
print >> outfile, "test_cases:"
yamlFile = open(os.path.join(TEST_RESOURCES_DIR, "pgts_browser_list.yaml"))
yamlContents = yaml.load(yamlFile, Loader=SafeLoader)
yamlFile.close()
for test_case in yamlContents["test_cases"]:
user_agent_string = test_case["user_agent_string"]
kwds = {}
if "js_ua" in test_case:
kwds = eval(test_case["js_ua"])
(family, major, minor, patch) = user_agent_parser.ParseUserAgent(
user_agent_string, **kwds
)
# Escape any double-quotes in the UA string
user_agent_string = re.sub(r'"', '\\"', user_agent_string)
print >> outfile, ' - user_agent_string: "' + user_agent_string + '"' + "\n" + ' family: "' + family + '"\n' + " major: " + (
"" if (major is None) else "'" + major + "'"
) + "\n" + " minor: " + (
"" if (minor is None) else "'" + minor + "'"
) + "\n" + " patch: " + (
"" if (patch is None) else "'" + patch + "'"
)
outfile.close()
# Run a set of test cases from a YAML file
def runUserAgentTestsFromYAML(self, file_name):
yamlFile = open(os.path.join(TEST_RESOURCES_DIR, file_name))
yamlContents = yaml.load(yamlFile, Loader=SafeLoader)
yamlFile.close()
for test_case in yamlContents["test_cases"]:
# Inputs to Parse()
user_agent_string = test_case["user_agent_string"]
kwds = {}
if "js_ua" in test_case:
kwds = eval(test_case["js_ua"])
# The expected results
expected = {
"family": test_case["family"],
"major": test_case["major"],
"minor": test_case["minor"],
"patch": test_case["patch"],
}
result = {}
result = user_agent_parser.ParseUserAgent(user_agent_string, **kwds)
self.assertEqual(
result,
expected,
"UA: {0}\n expected<{1}, {2}, {3}, {4}> != actual<{5}, {6}, {7}, {8}>".format(
user_agent_string,
expected["family"],
expected["major"],
expected["minor"],
expected["patch"],
result["family"],
result["major"],
result["minor"],
result["patch"],
),
)
def runOSTestsFromYAML(self, file_name):
yamlFile = open(os.path.join(TEST_RESOURCES_DIR, file_name))
yamlContents = yaml.load(yamlFile, Loader=SafeLoader)
yamlFile.close()
for test_case in yamlContents["test_cases"]:
# Inputs to Parse()
user_agent_string = test_case["user_agent_string"]
kwds = {}
if "js_ua" in test_case:
kwds = eval(test_case["js_ua"])
# The expected results
expected = {
"family": test_case["family"],
"major": test_case["major"],
"minor": test_case["minor"],
"patch": test_case["patch"],
"patch_minor": test_case["patch_minor"],
}
result = user_agent_parser.ParseOS(user_agent_string, **kwds)
self.assertEqual(
result,
expected,
"UA: {0}\n expected<{1} {2} {3} {4} {5}> != actual<{6} {7} {8} {9} {10}>".format(
user_agent_string,
expected["family"],
expected["major"],
expected["minor"],
expected["patch"],
expected["patch_minor"],
result["family"],
result["major"],
result["minor"],
result["patch"],
result["patch_minor"],
),
)
def runDeviceTestsFromYAML(self, file_name):
yamlFile = open(os.path.join(TEST_RESOURCES_DIR, file_name))
yamlContents = yaml.load(yamlFile, Loader=SafeLoader)
yamlFile.close()
for test_case in yamlContents["test_cases"]:
# Inputs to Parse()
user_agent_string = test_case["user_agent_string"]
kwds = {}
if "js_ua" in test_case:
kwds = eval(test_case["js_ua"])
# The expected results
expected = {
"family": test_case["family"],
"brand": test_case["brand"],
"model": test_case["model"],
}
result = user_agent_parser.ParseDevice(user_agent_string, **kwds)
self.assertEqual(
result,
expected,
"UA: {0}\n expected<{1} {2} {3}> != actual<{4} {5} {6}>".format(
user_agent_string,
expected["family"],
expected["brand"],
expected["model"],
result["family"],
result["brand"],
result["model"],
),
)
class GetFiltersTest(unittest.TestCase):
def testGetFiltersNoMatchesGiveEmptyDict(self):
user_agent_string = "foo"
filters = user_agent_parser.GetFilters(
user_agent_string, js_user_agent_string=None
)
self.assertEqual({}, filters)
def testGetFiltersJsUaPassedThrough(self):
user_agent_string = "foo"
filters = user_agent_parser.GetFilters(
user_agent_string, js_user_agent_string="bar"
)
self.assertEqual({"js_user_agent_string": "bar"}, filters)
def testGetFiltersJsUserAgentFamilyAndVersions(self):
user_agent_string = (
"Mozilla/4.0 (compatible; MSIE 8.0; "
"Windows NT 5.1; Trident/4.0; GTB6; .NET CLR 2.0.50727; "
".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"
)
filters = user_agent_parser.GetFilters(
user_agent_string, js_user_agent_string="bar", js_user_agent_family="foo"
)
self.assertEqual(
{"js_user_agent_string": "bar", "js_user_agent_family": "foo"}, filters
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,3 @@
VERSION = (2, 2, 0)
from .parsers import parse

View File

@@ -0,0 +1,14 @@
import sys
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str
def iteritems(d, **kw):
return iter(d.items(**kw))
else:
string_types = basestring
def iteritems(d, **kw):
return iter(d.iteritems(**kw))

View File

@@ -0,0 +1,268 @@
from collections import namedtuple
from ..ua_parser import user_agent_parser
from .compat import string_types
MOBILE_DEVICE_FAMILIES = (
'iPhone',
'iPod',
'Generic Smartphone',
'Generic Feature Phone',
'PlayStation Vita',
'iOS-Device'
)
PC_OS_FAMILIES = (
'Windows 95',
'Windows 98',
'Solaris',
)
MOBILE_OS_FAMILIES = (
'Windows Phone',
'Windows Phone OS', # Earlier versions of ua-parser returns Windows Phone OS
'Symbian OS',
'Bada',
'Windows CE',
'Windows Mobile',
'Maemo',
)
MOBILE_BROWSER_FAMILIES = (
'IE Mobile',
'Opera Mobile',
'Opera Mini',
'Chrome Mobile',
'Chrome Mobile WebView',
'Chrome Mobile iOS',
)
TABLET_DEVICE_FAMILIES = (
'iPad',
'BlackBerry Playbook',
'Blackberry Playbook', # Earlier versions of ua-parser returns "Blackberry" instead of "BlackBerry"
'Kindle',
'Kindle Fire',
'Kindle Fire HD',
'Galaxy Tab',
'Xoom',
'Dell Streak',
)
TOUCH_CAPABLE_OS_FAMILIES = (
'iOS',
'Android',
'Windows Phone',
'Windows CE',
'Windows Mobile',
'Firefox OS',
'MeeGo',
)
TOUCH_CAPABLE_DEVICE_FAMILIES = (
'BlackBerry Playbook',
'Blackberry Playbook',
'Kindle Fire',
)
EMAIL_PROGRAM_FAMILIES = set((
'Outlook',
'Windows Live Mail',
'AirMail',
'Apple Mail',
'Outlook',
'Thunderbird',
'Lightning',
'ThunderBrowse',
'Windows Live Mail',
'The Bat!',
'Lotus Notes',
'IBM Notes',
'Barca',
'MailBar',
'kmail2',
'YahooMobileMail'
))
def verify_attribute(attribute):
if isinstance(attribute, string_types) and attribute.isdigit():
return int(attribute)
return attribute
def parse_version(major=None, minor=None, patch=None, patch_minor=None):
# Returns version number tuple, attributes will be integer if they're numbers
major = verify_attribute(major)
minor = verify_attribute(minor)
patch = verify_attribute(patch)
patch_minor = verify_attribute(patch_minor)
return tuple(
filter(lambda x: x is not None, (major, minor, patch, patch_minor))
)
Browser = namedtuple('Browser', ['family', 'version', 'version_string'])
def parse_browser(family, major=None, minor=None, patch=None, patch_minor=None):
# Returns a browser object
version = parse_version(major, minor, patch)
version_string = '.'.join([str(v) for v in version])
return Browser(family, version, version_string)
OperatingSystem = namedtuple('OperatingSystem', ['family', 'version', 'version_string'])
def parse_operating_system(family, major=None, minor=None, patch=None, patch_minor=None):
version = parse_version(major, minor, patch)
version_string = '.'.join([str(v) for v in version])
return OperatingSystem(family, version, version_string)
Device = namedtuple('Device', ['family', 'brand', 'model'])
def parse_device(family, brand, model):
return Device(family, brand, model)
class UserAgent(object):
def __init__(self, user_agent_string):
ua_dict = user_agent_parser.Parse(user_agent_string)
self.ua_string = user_agent_string
self.os = parse_operating_system(**ua_dict['os'])
self.browser = parse_browser(**ua_dict['user_agent'])
self.device = parse_device(**ua_dict['device'])
def __str__(self):
return "{device} / {os} / {browser}".format(
device=self.get_device(),
os=self.get_os(),
browser=self.get_browser()
)
def __unicode__(self):
return unicode(str(self))
def _is_android_tablet(self):
# Newer Android tablets don't have "Mobile" in their user agent string,
# older ones like Galaxy Tab still have "Mobile" though they're not
if ('Mobile Safari' not in self.ua_string and
self.browser.family != "Firefox Mobile"):
return True
return False
def _is_blackberry_touch_capable_device(self):
# A helper to determine whether a BB phone has touch capabilities
# Blackberry Bold Touch series begins with 99XX
if 'Blackberry 99' in self.device.family:
return True
if 'Blackberry 95' in self.device.family: # BB Storm devices
return True
return False
def get_device(self):
return self.is_pc and "PC" or self.device.family
def get_os(self):
return ("%s %s" % (self.os.family, self.os.version_string)).strip()
def get_browser(self):
return ("%s %s" % (self.browser.family, self.browser.version_string)).strip()
@property
def is_tablet(self):
if self.device.family in TABLET_DEVICE_FAMILIES:
return True
if (self.os.family == 'Android' and self._is_android_tablet()):
return True
if self.os.family == 'Windows' and self.os.version_string.startswith('RT'):
return True
if self.os.family == 'Firefox OS' and 'Mobile' not in self.browser.family:
return True
return False
@property
def is_mobile(self):
# First check for mobile device and mobile browser families
if self.device.family in MOBILE_DEVICE_FAMILIES:
return True
if self.browser.family in MOBILE_BROWSER_FAMILIES:
return True
# Device is considered Mobile OS is Android and not tablet
# This is not fool proof but would have to suffice for now
if ((self.os.family == 'Android' or self.os.family == 'Firefox OS')
and not self.is_tablet):
return True
if self.os.family == 'BlackBerry OS' and self.device.family != 'Blackberry Playbook':
return True
if self.os.family in MOBILE_OS_FAMILIES:
return True
# TODO: remove after https://github.com/tobie/ua-parser/issues/126 is closed
if 'J2ME' in self.ua_string or 'MIDP' in self.ua_string:
return True
# This is here mainly to detect Google's Mobile Spider
if 'iPhone;' in self.ua_string:
return True
if 'Googlebot-Mobile' in self.ua_string:
return True
# Mobile Spiders should be identified as mobile
if self.device.family == 'Spider' and 'Mobile' in self.browser.family:
return True
# Nokia mobile
if 'NokiaBrowser' in self.ua_string and 'Mobile' in self.ua_string:
return True
return False
@property
def is_touch_capable(self):
# TODO: detect touch capable Nokia devices
if self.os.family in TOUCH_CAPABLE_OS_FAMILIES:
return True
if self.device.family in TOUCH_CAPABLE_DEVICE_FAMILIES:
return True
if self.os.family == 'Windows':
if self.os.version_string.startswith(('RT', 'CE')):
return True
if self.os.version_string.startswith('8') and 'Touch' in self.ua_string:
return True
if 'BlackBerry' in self.os.family and self._is_blackberry_touch_capable_device():
return True
return False
@property
def is_pc(self):
# Returns True for "PC" devices (Windows, Mac and Linux)
if 'Windows NT' in self.ua_string or self.os.family in PC_OS_FAMILIES or \
self.os.family == 'Windows' and self.os.version_string == 'ME':
return True
# TODO: remove after https://github.com/tobie/ua-parser/issues/127 is closed
if self.os.family == 'Mac OS X' and 'Silk' not in self.ua_string:
return True
# Maemo has 'Linux' and 'X11' in UA, but it is not for PC
if 'Maemo' in self.ua_string:
return False
if 'Chrome OS' in self.os.family:
return True
if 'Linux' in self.ua_string and 'X11' in self.ua_string:
return True
return False
@property
def is_bot(self):
return True if self.device.family == 'Spider' else False
@property
def is_email_client(self):
if self.browser.family in EMAIL_PROGRAM_FAMILIES:
return True
return False
def parse(user_agent_string):
return UserAgent(user_agent_string)

View File

@@ -0,0 +1,268 @@
import json
import os
import unittest
from ua_parser import user_agent_parser
from . import compat
from .parsers import parse
iphone_ua_string = 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B179 Safari/7534.48.3'
ipad_ua_string = 'Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B314 Safari/531.21.10'
galaxy_tab_ua_string = 'Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1'
galaxy_s3_ua_string = 'Mozilla/5.0 (Linux; U; Android 4.0.4; en-gb; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'
kindle_fire_ua_string = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.1.0-80) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true'
playbook_ua_string = 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.0.1; en-US) AppleWebKit/535.8+ (KHTML, like Gecko) Version/7.2.0.1 Safari/535.8+'
nexus_7_ua_string = 'Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19'
windows_phone_ua_string = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; SAMSUNG; SGH-i917)'
blackberry_torch_ua_string = 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; zh-TW) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.448 Mobile Safari/534.8+'
blackberry_bold_ua_string = 'BlackBerry9700/5.0.0.862 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/331 UNTRUSTED/1.0 3gpp-gba'
blackberry_bold_touch_ua_string = 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9930; en-US) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.0.0.241 Mobile Safari/534.11+'
windows_rt_ua_string = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; ARM; Trident/6.0)'
j2me_opera_ua_string = 'Opera/9.80 (J2ME/MIDP; Opera Mini/9.80 (J2ME/22.478; U; en) Presto/2.5.25 Version/10.54'
ie_ua_string = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)'
ie_touch_ua_string = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0; Touch)'
mac_safari_ua_string = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2'
windows_ie_ua_string = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)'
ubuntu_firefox_ua_string = 'Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1'
google_bot_ua_string = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
nokia_n97_ua_string = 'Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/12.0.024; Profile/MIDP-2.1 Configuration/CLDC-1.1; en-us) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.12344'
android_firefox_aurora_ua_string = 'Mozilla/5.0 (Android; Mobile; rv:27.0) Gecko/27.0 Firefox/27.0'
thunderbird_ua_string = 'Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Thunderbird/38.2.0 Lightning/4.0.2'
outlook_usa_string = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; Microsoft Outlook 15.0.4420)'
chromebook_ua_string = 'Mozilla/5.0 (X11; CrOS i686 0.12.433) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.77 Safari/534.30'
iphone_ua = parse(iphone_ua_string)
ipad_ua = parse(ipad_ua_string)
galaxy_tab = parse(galaxy_tab_ua_string)
galaxy_s3_ua = parse(galaxy_s3_ua_string)
kindle_fire_ua = parse(kindle_fire_ua_string)
playbook_ua = parse(playbook_ua_string)
nexus_7_ua = parse(nexus_7_ua_string)
windows_phone_ua = parse(windows_phone_ua_string)
windows_rt_ua = parse(windows_rt_ua_string)
blackberry_torch_ua = parse(blackberry_torch_ua_string)
blackberry_bold_ua = parse(blackberry_bold_ua_string)
blackberry_bold_touch_ua = parse(blackberry_bold_touch_ua_string)
j2me_opera_ua = parse(j2me_opera_ua_string)
ie_ua = parse(ie_ua_string)
ie_touch_ua = parse(ie_touch_ua_string)
mac_safari_ua = parse(mac_safari_ua_string)
windows_ie_ua = parse(windows_ie_ua_string)
ubuntu_firefox_ua = parse(ubuntu_firefox_ua_string)
google_bot_ua = parse(google_bot_ua_string)
nokia_n97_ua = parse(nokia_n97_ua_string)
android_firefox_aurora_ua = parse(android_firefox_aurora_ua_string)
thunderbird_ua = parse(thunderbird_ua_string)
outlook_ua = parse(outlook_usa_string)
chromebook_ua = parse(chromebook_ua_string)
class UserAgentsTest(unittest.TestCase):
def test_user_agent_object_assignments(self):
ua_dict = user_agent_parser.Parse(devices['iphone']['ua_string'])
iphone_ua = devices['iphone']['user_agent']
# Ensure browser attributes are assigned correctly
self.assertEqual(iphone_ua.browser.family,
ua_dict['user_agent']['family'])
self.assertEqual(
iphone_ua.browser.version,
(int(ua_dict['user_agent']['major']),
int(ua_dict['user_agent']['minor']))
)
# Ensure os attributes are assigned correctly
self.assertEqual(iphone_ua.os.family, ua_dict['os']['family'])
self.assertEqual(
iphone_ua.os.version,
(int(ua_dict['os']['major']), int(ua_dict['os']['minor']))
)
# Ensure device attributes are assigned correctly
self.assertEqual(iphone_ua.device.family,
ua_dict['device']['family'])
def test_is_tablet_property(self):
self.assertFalse(iphone_ua.is_tablet)
self.assertFalse(galaxy_s3_ua.is_tablet)
self.assertFalse(blackberry_torch_ua.is_tablet)
self.assertFalse(blackberry_bold_ua.is_tablet)
self.assertFalse(windows_phone_ua.is_tablet)
self.assertFalse(ie_ua.is_tablet)
self.assertFalse(ie_touch_ua.is_tablet)
self.assertFalse(mac_safari_ua.is_tablet)
self.assertFalse(windows_ie_ua.is_tablet)
self.assertFalse(ubuntu_firefox_ua.is_tablet)
self.assertFalse(j2me_opera_ua.is_tablet)
self.assertFalse(google_bot_ua.is_tablet)
self.assertFalse(nokia_n97_ua.is_tablet)
self.assertTrue(windows_rt_ua.is_tablet)
self.assertTrue(ipad_ua.is_tablet)
self.assertTrue(playbook_ua.is_tablet)
self.assertTrue(kindle_fire_ua.is_tablet)
self.assertTrue(nexus_7_ua.is_tablet)
self.assertFalse(android_firefox_aurora_ua.is_tablet)
def test_is_mobile_property(self):
self.assertTrue(iphone_ua.is_mobile)
self.assertTrue(galaxy_s3_ua.is_mobile)
self.assertTrue(blackberry_torch_ua.is_mobile)
self.assertTrue(blackberry_bold_ua.is_mobile)
self.assertTrue(windows_phone_ua.is_mobile)
self.assertTrue(j2me_opera_ua.is_mobile)
self.assertTrue(nokia_n97_ua.is_mobile)
self.assertFalse(windows_rt_ua.is_mobile)
self.assertFalse(ipad_ua.is_mobile)
self.assertFalse(playbook_ua.is_mobile)
self.assertFalse(kindle_fire_ua.is_mobile)
self.assertFalse(nexus_7_ua.is_mobile)
self.assertFalse(ie_ua.is_mobile)
self.assertFalse(ie_touch_ua.is_mobile)
self.assertFalse(mac_safari_ua.is_mobile)
self.assertFalse(windows_ie_ua.is_mobile)
self.assertFalse(ubuntu_firefox_ua.is_mobile)
self.assertFalse(google_bot_ua.is_mobile)
self.assertTrue(android_firefox_aurora_ua.is_mobile)
def test_is_touch_property(self):
self.assertTrue(iphone_ua.is_touch_capable)
self.assertTrue(galaxy_s3_ua.is_touch_capable)
self.assertTrue(ipad_ua.is_touch_capable)
self.assertTrue(playbook_ua.is_touch_capable)
self.assertTrue(kindle_fire_ua.is_touch_capable)
self.assertTrue(nexus_7_ua.is_touch_capable)
self.assertTrue(windows_phone_ua.is_touch_capable)
self.assertTrue(ie_touch_ua.is_touch_capable)
self.assertTrue(blackberry_bold_touch_ua.is_mobile)
self.assertTrue(blackberry_torch_ua.is_mobile)
self.assertFalse(j2me_opera_ua.is_touch_capable)
self.assertFalse(ie_ua.is_touch_capable)
self.assertFalse(blackberry_bold_ua.is_touch_capable)
self.assertFalse(mac_safari_ua.is_touch_capable)
self.assertFalse(windows_ie_ua.is_touch_capable)
self.assertFalse(ubuntu_firefox_ua.is_touch_capable)
self.assertFalse(google_bot_ua.is_touch_capable)
self.assertFalse(nokia_n97_ua.is_touch_capable)
self.assertTrue(android_firefox_aurora_ua.is_touch_capable)
def test_is_pc(self):
self.assertFalse(iphone_ua.is_pc)
self.assertFalse(galaxy_s3_ua.is_pc)
self.assertFalse(ipad_ua.is_pc)
self.assertFalse(playbook_ua.is_pc)
self.assertFalse(kindle_fire_ua.is_pc)
self.assertFalse(nexus_7_ua.is_pc)
self.assertFalse(windows_phone_ua.is_pc)
self.assertFalse(blackberry_bold_touch_ua.is_pc)
self.assertFalse(blackberry_torch_ua.is_pc)
self.assertFalse(blackberry_bold_ua.is_pc)
self.assertFalse(j2me_opera_ua.is_pc)
self.assertFalse(google_bot_ua.is_pc)
self.assertFalse(nokia_n97_ua.is_pc)
self.assertTrue(mac_safari_ua.is_pc)
self.assertTrue(windows_ie_ua.is_pc)
self.assertTrue(ubuntu_firefox_ua.is_pc)
self.assertTrue(ie_touch_ua.is_pc)
self.assertTrue(ie_ua.is_pc)
self.assertFalse(android_firefox_aurora_ua.is_pc)
self.assertTrue(chromebook_ua.is_pc)
def test_is_bot(self):
self.assertTrue(google_bot_ua.is_bot)
self.assertFalse(iphone_ua.is_bot)
self.assertFalse(galaxy_s3_ua.is_bot)
self.assertFalse(ipad_ua.is_bot)
self.assertFalse(playbook_ua.is_bot)
self.assertFalse(kindle_fire_ua.is_bot)
self.assertFalse(nexus_7_ua.is_bot)
self.assertFalse(windows_phone_ua.is_bot)
self.assertFalse(blackberry_bold_touch_ua.is_bot)
self.assertFalse(blackberry_torch_ua.is_bot)
self.assertFalse(blackberry_bold_ua.is_bot)
self.assertFalse(j2me_opera_ua.is_bot)
self.assertFalse(mac_safari_ua.is_bot)
self.assertFalse(windows_ie_ua.is_bot)
self.assertFalse(ubuntu_firefox_ua.is_bot)
self.assertFalse(ie_touch_ua.is_bot)
self.assertFalse(ie_ua.is_bot)
self.assertFalse(nokia_n97_ua.is_bot)
self.assertFalse(android_firefox_aurora_ua.is_bot)
def test_is_email_client(self):
self.assertTrue(thunderbird_ua.is_email_client)
self.assertTrue(outlook_ua.is_email_client)
self.assertFalse(playbook_ua.is_email_client)
self.assertFalse(kindle_fire_ua.is_email_client)
self.assertFalse(nexus_7_ua.is_email_client)
self.assertFalse(windows_phone_ua.is_email_client)
self.assertFalse(blackberry_bold_touch_ua.is_email_client)
self.assertFalse(blackberry_torch_ua.is_email_client)
self.assertFalse(blackberry_bold_ua.is_email_client)
self.assertFalse(j2me_opera_ua.is_email_client)
self.assertFalse(mac_safari_ua.is_email_client)
self.assertFalse(windows_ie_ua.is_email_client)
self.assertFalse(ubuntu_firefox_ua.is_email_client)
self.assertFalse(ie_touch_ua.is_email_client)
self.assertFalse(ie_ua.is_email_client)
self.assertFalse(nokia_n97_ua.is_email_client)
self.assertFalse(android_firefox_aurora_ua.is_email_client)
def test_strings(self):
self.assertEqual(str(iphone_ua), "iPhone / iOS 5.1 / Mobile Safari 5.1")
self.assertEqual(str(ipad_ua), "iPad / iOS 3.2 / Mobile Safari 4.0.4")
self.assertEqual(str(galaxy_tab), "Samsung SCH-I800 / Android 2.2 / Android 2.2")
self.assertEqual(str(galaxy_s3_ua), "Samsung GT-I9300 / Android 4.0.4 / Android 4.0.4")
self.assertEqual(str(kindle_fire_ua), "Kindle / Android / Amazon Silk 1.1.0-80")
self.assertEqual(str(playbook_ua), "BlackBerry Playbook / BlackBerry Tablet OS 2.0.1 / BlackBerry WebKit 2.0.1")
self.assertEqual(str(nexus_7_ua), "Asus Nexus 7 / Android 4.1.1 / Chrome 18.0.1025")
self.assertEqual(str(windows_phone_ua), "Samsung SGH-i917 / Windows Phone 7.5 / IE Mobile 9.0")
self.assertEqual(str(windows_rt_ua), "PC / Windows RT / IE 10.0")
self.assertEqual(str(blackberry_torch_ua), "BlackBerry 9800 / BlackBerry OS 6.0.0 / BlackBerry WebKit 6.0.0")
self.assertEqual(str(blackberry_bold_ua), "BlackBerry 9700 / BlackBerry OS 5.0.0 / BlackBerry 9700")
self.assertEqual(str(blackberry_bold_touch_ua), "BlackBerry 9930 / BlackBerry OS 7.0.0 / BlackBerry WebKit 7.0.0")
self.assertEqual(str(j2me_opera_ua), "Generic Feature Phone / Other / Opera Mini 9.80")
self.assertEqual(str(ie_ua), "PC / Windows 8 / IE 10.0")
self.assertEqual(str(ie_touch_ua), "PC / Windows 8 / IE 10.0")
self.assertEqual(str(mac_safari_ua), "PC / Mac OS X 10.6.8 / WebKit Nightly 537.13")
self.assertEqual(str(windows_ie_ua), "PC / Windows 7 / IE 9.0")
self.assertEqual(str(ubuntu_firefox_ua), "PC / Ubuntu / Firefox 15.0.1")
self.assertEqual(str(google_bot_ua), "Spider / Other / Googlebot 2.1")
self.assertEqual(str(nokia_n97_ua), "Nokia N97 / Symbian OS 9.4 / Nokia Browser 7.1.12344")
self.assertEqual(str(android_firefox_aurora_ua), "Generic Smartphone / Android / Firefox Mobile 27.0")
def test_unicode_strings(self):
try:
# Python 2
unicode_ua_str = unicode(devices['iphone']['user_agent'])
self.assertEqual(unicode_ua_str,
u"iPhone / iOS 5.1 / Mobile Safari 5.1")
self.assertTrue(isinstance(unicode_ua_str, unicode))
except NameError:
# Python 3
unicode_ua_str = str(devices['iphone']['user_agent'])
self.assertEqual(unicode_ua_str,
"iPhone / iOS 5.1 / Mobile Safari 5.1")
with open(os.path.join(os.path.dirname(__file__), 'devices.json')) as f:
devices = json.load(f)
def test_wrapper(items):
def test_func(self):
attrs = ('is_bot', 'is_mobile',
'is_pc', 'is_tablet', 'is_touch_capable')
for attr in attrs:
self.assertEqual(
getattr(items['user_agent'], attr), items[attr], msg=attr)
# Temporarily commenting this out since UserAgent.device
# may return different string depending ua-parser version
# self.assertEqual(str(items['user_agent']), items['str'])
return test_func
for device, items in compat.iteritems(devices):
items['user_agent'] = parse(items['ua_string'])
setattr(UserAgentsTest, 'test_' + device, test_wrapper(items))

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Part of odooai.cn. See LICENSE file for full copyright and licensing details.
# Created on 2023-02-02
# author: 欧度智能http://www.odooai.cn
# email: 300883@qq.com
# resource of odooai
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
# Odoo12在线用户手册长期更新
# http://www.odooai.cn/documentation/user/12.0/en/index.html
# Odoo12在线开发者手册长期更新
# http://www.odooai.cn/documentation/12.0/index.html
# Odoo10在线中文用户手册长期更新
# http://www.odooai.cn/documentation/user/10.0/zh_CN/index.html
# Odoo10离线中文用户手册下载
# http://www.odooai.cn/odoo10_user_manual_document_offline/
# Odoo10离线开发手册下载-含python教程jquery参考Jinja2模板PostgresSQL参考odoo开发必备
# http://www.odooai.cn/odoo10_developer_document_offline/
# description:
from . import base
from . import ir_ui_view
from . import ir_cron
from . import res_users
from . import ir_mail_server
from . import mail_mail
from . import ir_http
from . import app_import
from . import res_partner

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
import base64
import io
import csv
import os.path
from odoo import api, fields, models, modules, tools, SUPERUSER_ID, _
from odoo.tools import pycompat
from odoo.tests import common
ADMIN_USER_ID = common.ADMIN_USER_ID
def app_quick_import(cr, content_path, sep=None):
if not sep:
sep = '/'
dir_split = content_path.split(sep)
module_name = dir_split[0]
file_name = dir_split[2]
file_path, file_type = os.path.splitext(content_path)
model_name = file_name.replace(file_type, '')
file_path = modules.get_module_resource(module_name, dir_split[1], file_name)
content = open(file_path, 'rb').read()
uid = SUPERUSER_ID
if model_name == 'discuss.channel':
# todo: 创建discuss.channel时如果用root用户会报错
uid = 2
env = api.Environment(cr, uid, {})
if file_type == '.csv':
file_type = 'text/csv'
elif file_type in ['.xls', '.xlsx']:
file_type = 'application/vnd.ms-excel'
import_wizard = env['base_import.import'].create({
'res_model': model_name,
'file_name': file_name,
'file_type': file_type,
'file': content,
})
if file_type == 'text/csv':
preview = import_wizard.parse_preview({
'separator': ',',
'has_headers': True,
'quoting': '"',
})
elif file_type == 'application/vnd.ms-excel':
preview = import_wizard.parse_preview({
'has_headers': True,
})
result = import_wizard.execute_import(
preview["headers"],
preview["headers"],
preview["options"]
)

242
app_common/models/base.py Normal file
View File

@@ -0,0 +1,242 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from odoo.http import request
import requests
import base64
from io import BytesIO
import uuid
from datetime import date, datetime, time
import pytz
import logging
_logger = logging.getLogger(__name__)
# 常规的排除的fields
EXCLU_FIELDS = [
'__last_update',
'access_token',
'access_url',
'access_warning',
'activity_date_deadline',
'activity_exception_decoration',
'activity_exception_icon',
'activity_ids',
'activity_state',
'activity_summary',
'activity_type_id',
'activity_user_id',
'display_name',
'message_attachment_count',
'message_channel_ids',
'message_follower_ids',
'message_has_error',
'message_has_error_counter',
'message_has_sms_error',
'message_ids',
'message_is_follower',
'message_main_attachment_id',
'message_needaction',
'message_needaction_counter',
'message_partner_ids',
'message_unread',
'message_unread_counter',
'website_message_ids',
'write_date',
'write_uid',
]
class Base(models.AbstractModel):
_inherit = 'base'
@api.model
def _app_check_sys_op(self):
if self.env.user.has_group('base.group_erp_manager'):
return True
return False
@api.model
def _get_normal_fields(self):
f_list = []
for k, v in self._fields.items():
if k not in EXCLU_FIELDS:
f_list.append(k)
return f_list
@api.model
def _app_get_m2o_default(self, fieldname, domain=[]):
if hasattr(self, fieldname) and self._fields[fieldname].type == 'many2one':
if self._context.get(fieldname) or self._context.get('default_%s' % fieldname):
return self._context.get(fieldname) or self._context.get('default_%s' % fieldname)
else:
if not domain:
domain = self._fields[fieldname].domain or []
rec = self.env[self._fields[fieldname].comodel_name].sudo().search(domain, limit=1)
return rec.id if rec else False
return False
def _app_dt2local(self, value, return_format=DEFAULT_SERVER_DATETIME_FORMAT):
"""
将value中时间按格式转为用户本地时间.注意只处理in str为字符串类型,如果是时间类型直接用 datetime.now(tz)
"""
if not value:
return value
if isinstance(value, datetime):
value = value.strftime(return_format)
dt = datetime.strptime(value, return_format)
user_tz = pytz.timezone(self.env.user.tz or 'Etc/GMT-8')
_logger.warning('============= user2 tz: %s' % user_tz)
dt = dt.replace(tzinfo=pytz.timezone('UTC'))
return dt.astimezone(user_tz).strftime(return_format)
def _app_dt2utc(self, value, return_format=DEFAULT_SERVER_DATETIME_FORMAT):
"""
将value中用户本地时间按格式转为UTC时间输出 str
"""
if not value:
return value
if isinstance(value, datetime):
value = value.strftime(return_format)
dt = datetime.strptime(value, return_format)
pytz_timezone = pytz.timezone('Etc/GMT+8')
dt = dt.replace(tzinfo=pytz.timezone('UTC'))
return dt.astimezone(pytz_timezone).strftime(return_format)
@api.model
def _get_image_from_url(self, url):
# 返回这个图片的base64编码
if not self._app_check_sys_op():
return False
return get_image_from_url(url)
@api.model
def _get_image_url2attachment(self, url, mimetype_list=None):
# Todo: mimetype filter
if not self._app_check_sys_op():
return False
image, file_name = get_image_url2attachment(url)
if image and file_name:
try:
attachment = self.env['ir.attachment'].create({
'datas': image,
'name': file_name,
'website_id': False,
'res_model': self._name,
'res_id': self.id,
})
attachment.generate_access_token()
return attachment
except Exception as e:
_logger.error('get_image_url2attachment error: %s' % str(e))
return False
else:
return False
@api.model
def _get_image_base642attachment(self, data):
if not self._app_check_sys_op():
return False
image, file_name = get_image_base642attachment(data)
if image and file_name:
try:
attachment = self.env['ir.attachment'].create({
'datas': image,
'name': file_name,
'website_id': False,
'res_model': self._name,
'res_id': self.id,
})
attachment.generate_access_token()
return attachment
except Exception as e:
_logger.error('get_image_base642attachment error: %s' % str(e))
return False
else:
return False
def get_ua_type(self):
return get_ua_type()
def get_image_from_url(url):
if not url:
return None
try:
response = requests.get(url, timeout=5)
except Exception as e:
return None
# 返回这个图片的base64编码
return base64.b64encode(BytesIO(response.content).read())
def get_image_url2attachment(url):
if not url:
return None
try:
if url.startswith('//'):
url = 'https:%s' % url
response = requests.get(url, timeout=30)
except Exception as e:
return None, None
# 返回这个图片的base64编码
image = base64.b64encode(BytesIO(response.content).read())
file_name = url.split('/')[-1]
return image, file_name
def get_image_base642attachment(data):
if not data:
return None
try:
image_data = data.split(',')[1]
file_name = str(uuid.uuid4()) + '.png'
return image_data, file_name
except Exception as e:
return None, None
def get_ua_type():
ua = request.httprequest.headers.get('User-Agent')
# 临时用 agent 处理,后续要前端中正确处理或者都从后台来
# 微信浏览器
# MicroMessenger: Mozilla/5.0 (Linux; Android 10; ELE-AL00 Build/HUAWEIELE-AL00; wv)
# AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/77.0.3865.120
# MQQBrowser/6.2 TBS/045525 Mobile Safari/537.36 MMWEBID/3135 MicroMessenger/8.0.2.1860(0x2800023B) Process/tools WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64
# 微信浏览器,开发工具,网页 iphone
# ,Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
# wechatdevtools/1.03.2011120 MicroMessenger/7.0.4 Language/zh_CN webview/16178807094901773
# webdebugger port/27772 token/b91f4a234b918f4e2a5d1a835a09c31e
# 微信小程序
# MicroMessenger: Mozilla/5.0 (Linux; Android 10; ELE-AL00 Build/HUAWEIELE-AL00; wv)
# AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.62 XWEB/2767 MMWEBSDK/20210302 Mobile Safari/537.36 MMWEBID/6689 MicroMessenger/8.0.2.1860(0x2800023B) Process/appbrand2 WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64
# MiniProgramEnv/android
# 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_7_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.37(0x18002529) NetType/WIFI Language/zh_CN'
# 微信浏览器开发工具小程序iphone
# Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
# wechatdevtools/1.03.2011120 MicroMessenger/7.0.4 Language/zh_CN webview/
# 微信内iphone web
# Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
# MicroMessenger/8.0.3(0x1800032a) NetType/WIFI Language/zh_CN
# 安卓app,h5
# ELE-AL00(Android/10) (cn.erpapp.o20sticks.App/13.20.12.09) Weex/0.26.0 1080x2265
# web 表示普通浏览器,后续更深入处理
utype = 'web'
# todo: 引入现成 py lib处理企业微信
if 'MicroMessenger' in ua and 'webdebugger' not in ua \
and ('miniProgram' in ua or 'MiniProgram' in ua or 'MiniProgramEnv' in ua or 'wechatdevtools' in ua):
# 微信小程序及开发者工具
utype = 'wxapp'
elif 'MicroMessenger' in ua:
# 微信浏览器
utype = 'wxweb'
elif 'cn.erpapp.o20sticks.App' in ua:
# 安卓app
utype = 'native_android'
# _logger.warning('=========get ua %s,%s' % (utype, ua))
return utype

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, fields, models, modules, tools, _
_logger = logging.getLogger(__name__)
class IrCron(models.Model):
_inherit = "ir.cron"
trigger_user_id = fields.Many2one('res.users', string='Last Trigger User')
def method_direct_trigger(self):
self.write({'trigger_user_id': self.env.user.id})
return super(IrCron, self).method_direct_trigger()

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.http import request
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
def session_info(self):
result = super(IrHttp, self).session_info()
result['ua_type'] = self.get_ua_type()
return result

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
import logging
_logger = logging.getLogger(__name__)
class IrMailServer(models.Model):
_inherit = "ir.mail_server"
_order = "sequence"
# 改默认发邮件逻辑
@api.model
def send_email(self, message, mail_server_id=None, smtp_server=None, smtp_port=None,
smtp_user=None, smtp_password=None, smtp_encryption=None,
smtp_ssl_certificate=None, smtp_ssl_private_key=None,
smtp_debug=False, smtp_session=None):
email_to = message['To']
# 忽略掉无效email避免被ban
if email_to:
if email_to.find('no-reply@odooai.cn') != -1 or email_to.find('postmaster-odoo@odooai.cn') != -1:
pass
elif email_to.find('example.com') != -1 or email_to.find('@sunpop.cn') != -1 or email_to.find('@odooapp.cn') != -1:
_logger.error(_("=================Email to ignore: %s") % email_to)
raise AssertionError(_("Email to ignore: %s") % email_to)
return super(IrMailServer, self).send_email(message, mail_server_id, smtp_server, smtp_port,
smtp_user, smtp_password, smtp_encryption, smtp_ssl_certificate, smtp_ssl_private_key,
smtp_debug, smtp_session)

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from odoo import api, models, tools, SUPERUSER_ID
from odoo.modules.module import get_resource_path
from odoo.tools import view_validation
from odoo.tools.view_validation import _relaxng_cache, validate, _validators
from odoo.tools.safe_eval import safe_eval
from lxml import etree
import logging
_logger = logging.getLogger(__name__)
def app_relaxng(view_type):
""" Return a validator for the given view type, or None. """
if view_type not in _relaxng_cache:
# tree, search 特殊
if view_type in ['tree', 'search']:
_file = get_resource_path('app_common', 'rng', '%s_view.rng' % view_type)
else:
_file = get_resource_path('base', 'rng', '%s_view.rng' % view_type)
with tools.file_open(_file) as frng:
try:
relaxng_doc = etree.parse(frng)
_relaxng_cache[view_type] = etree.RelaxNG(relaxng_doc)
except Exception:
_logger.error('You can Ignore this. Failed to load RelaxNG XML schema for views validation')
_relaxng_cache[view_type] = None
return _relaxng_cache[view_type]
# view_validation.relaxng = app_relaxng
#
# class View(models.Model):
# _inherit = 'ir.ui.view'
#
# def __init__(self, env, ids, prefetch_ids):
# # 这里应该是无必要,但为了更安全
# super(View, self).__init__(env, ids, prefetch_ids)
# view_validation.relaxng = app_relaxng
#
# # todo: 有可能需要处理增加的 header等标签
# # 直接重写原生方法
# # def transfer_node_to_modifiers(node, modifiers, context=None, in_tree_view=False):

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
import logging
_logger = logging.getLogger(__name__)
class MailMail(models.Model):
_inherit = "mail.mail"
# 猴子补丁模式,改默认发邮件逻辑
def _send(self, auto_commit=False, raise_exception=False, smtp_session=None, alias_domain_id=False):
for m in self:
email_to = m.email_to
# 忽略掉无效email避免被ban
if email_to:
if email_to.find('no-reply@odooai.cn') != -1 or email_to.find('postmaster-odoo@odooai.cn') != -1:
pass
elif email_to.find('example.com') != -1 or email_to.find('@sunpop.cn') != -1 or email_to.find('@odooapp.cn') != -1:
_logger.error(_("=================Email to ignore: %s") % email_to)
self = self - m
if not self:
return True
return super(MailMail, self)._send(auto_commit, raise_exception, smtp_session, alias_domain_id)

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools, _
class ResPartner(models.Model):
_inherit = 'res.partner'
def get_related_user_id(self):
self.ensure_one()
user = self.env['res.users'].sudo().with_context(active_test=False).search([('partner_id', '=', self.id)], limit=1)
if not user and self.commercial_partner_id:
user = self.env['res.users'].sudo().with_context(active_test=False).search([('partner_id', '=', self.commercial_partner_id.id)],
limit=1)
return user

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools, _
class ResUsers(models.Model):
_inherit = 'res.users'
login = fields.Char(index=True)

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

428
app_common/rng/common.rng Normal file
View File

@@ -0,0 +1,428 @@
<?xml version="1.0" encoding="UTF-8"?>
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/annotation/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<!-- Handling of element overloading when inheriting from a base
template
-->
<rng:define name="overload">
<rng:optional>
<!--
Alter matched element with content
-->
<rng:choice>
<rng:attribute name="position">
<rng:choice>
<!-- Insert content before first child -->
<rng:value>before</rng:value>
<!-- Insert content after last child -->
<rng:value>after</rng:value>
<!-- Replace all children with content -->
<rng:value>inside</rng:value>
<!-- Replace matched element itself with content -->
<rng:value>replace</rng:value>
</rng:choice>
</rng:attribute>
<rng:group>
<rng:attribute name="position">
<!-- Edit element attributes -->
<rng:value>attributes</rng:value>
</rng:attribute>
<rng:oneOrMore>
<rng:element name="attribute">
<rng:attribute name="name"><rng:text/></rng:attribute>
<rng:text />
</rng:element>
</rng:oneOrMore>
</rng:group>
</rng:choice>
</rng:optional>
</rng:define>
<rng:define name="access_rights">
<rng:optional>
<rng:attribute name="groups"/>
</rng:optional>
</rng:define>
<rng:define name="container">
<rng:optional><rng:attribute name="col"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="field"/>
<rng:ref name="group"/>
<rng:ref name="button"/>
<rng:ref name="label" />
<rng:ref name="separator"/>
<rng:ref name="image"/>
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:define>
<rng:define name="image">
<rng:element name="image">
<rng:attribute name="name"/>
</rng:element>
</rng:define>
<rng:define name="html">
<rng:element name="html">
<rng:zeroOrMore>
<rng:text/>
<rng:ref name="any"/>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="label">
<rng:element name="label">
<rng:ref name="overload"/>
<rng:ref name="access_rights"/>
<rng:optional><rng:attribute name="invisible"/></rng:optional>
<rng:optional><rng:attribute name="align"/></rng:optional>
<rng:optional><rng:attribute name="nolabel"/></rng:optional>
<rng:optional><rng:attribute name="colspan"/></rng:optional>
<rng:optional><rng:attribute name="string"/></rng:optional>
<rng:optional><rng:attribute name="angle"/></rng:optional>
<rng:optional><rng:attribute name="fill"/></rng:optional>
<rng:optional><rng:attribute name="help"/></rng:optional>
<rng:optional><rng:attribute name="width"/></rng:optional>
<rng:optional><rng:attribute name="wrap"/></rng:optional>
<rng:optional><rng:attribute name="name"/></rng:optional>
<rng:optional>
<!-- @for: allows to explicitely link a label to a field -->
<rng:attribute name="for"/>
</rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:text/>
<rng:ref name="field"/>
<rng:ref name="group"/>
<rng:ref name="button"/>
<rng:ref name="label" />
<rng:ref name="separator"/>
<rng:ref name="image"/>
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="any">
<rng:element>
<rng:anyName/>
<rng:zeroOrMore>
<rng:choice>
<rng:attribute>
<rng:anyName/>
</rng:attribute>
<rng:text/>
<rng:ref name="any"/>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="separator">
<rng:element name="separator">
<rng:ref name="overload"/>
<rng:ref name="access_rights"/>
<rng:optional><rng:attribute name="invisible"/></rng:optional>
<rng:optional><rng:attribute name="name"/></rng:optional>
<rng:optional><rng:attribute name="colspan"/></rng:optional>
<rng:optional><rng:attribute name="rowspan"/></rng:optional>
<rng:optional><rng:attribute name="string"/></rng:optional>
<rng:optional><rng:attribute name="col"/></rng:optional>
<rng:optional><rng:attribute name="select"/></rng:optional>
<rng:optional><rng:attribute name="orientation"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="separator"/>
<rng:ref name="button"/>
<rng:ref name="field"/>
<rng:ref name="label" />
<rng:ref name="group" />
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="xpath">
<rng:element name="xpath">
<rng:optional><rng:attribute name="expr"/></rng:optional>
<rng:ref name="overload"/>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="any"/>
<rng:ref name="button"/>
<rng:ref name="html"/>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="data">
<rng:element name="data">
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="field"/>
<rng:ref name="label"/>
<rng:ref name="separator"/>
<rng:ref name="xpath"/>
<rng:ref name="button"/>
<rng:ref name="group"/>
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="field">
<rng:element name="field">
<rng:attribute name="name" />
<rng:ref name="overload"/>
<rng:ref name="access_rights"/>
<rng:optional><rng:attribute name="domain_filter"/></rng:optional>
<rng:optional><rng:attribute name="class"/></rng:optional>
<rng:optional><rng:attribute name="string"/></rng:optional>
<rng:optional><rng:attribute name="completion"/></rng:optional>
<rng:optional><rng:attribute name="width"/></rng:optional>
<rng:optional><rng:attribute name="type"/></rng:optional>
<rng:optional><rng:attribute name="ref"/></rng:optional>
<rng:optional><rng:attribute name="eval"/></rng:optional>
<rng:optional><rng:attribute name="search"/></rng:optional>
<rng:optional><rng:attribute name="model"/></rng:optional>
<rng:optional><rng:attribute name="use"/></rng:optional>
<rng:optional><rng:attribute name="on_change"/></rng:optional>
<rng:optional><rng:attribute name="domain"/></rng:optional>
<rng:optional><rng:attribute name="filter_domain"/></rng:optional>
<rng:optional><rng:attribute name="column_invisible"/></rng:optional>
<rng:optional><rng:attribute name="invisible"/></rng:optional>
<rng:optional><rng:attribute name="password"/></rng:optional>
<rng:optional><rng:attribute name="comparator"/></rng:optional>
<rng:optional><rng:attribute name="sum"/></rng:optional>
<rng:optional><rng:attribute name="bold"/></rng:optional>
<rng:optional><rng:attribute name="avg"/></rng:optional>
<rng:optional><rng:attribute name="select"/></rng:optional>
<rng:optional><rng:attribute name="hierarchize"/></rng:optional>
<rng:optional><rng:attribute name="expand"/></rng:optional>
<rng:optional><rng:attribute name="group"/></rng:optional>
<rng:optional><rng:attribute name="color"/></rng:optional>
<rng:optional><rng:attribute name="groupby"/></rng:optional>
<rng:optional><rng:attribute name="enable_counters"/></rng:optional>
<rng:optional><rng:attribute name="limit"/></rng:optional>
<rng:optional><rng:attribute name="operator"/></rng:optional>
<rng:optional><rng:attribute name="colspan"/></rng:optional>
<rng:optional><rng:attribute name="nolabel"/></rng:optional>
<rng:optional><rng:attribute name="required"/></rng:optional>
<rng:optional><rng:attribute name="readonly"/></rng:optional>
<rng:optional><rng:attribute name="view_mode"/></rng:optional>
<rng:optional><rng:attribute name="widget"/></rng:optional>
<rng:optional><rng:attribute name="context"/></rng:optional>
<rng:optional><rng:attribute name="digits"/></rng:optional>
<rng:optional><rng:attribute name="icon"/></rng:optional>
<rng:optional><rng:attribute name="mode"/></rng:optional>
<rng:optional><rng:attribute name="size"/></rng:optional>
<rng:optional><rng:attribute name="filename"/></rng:optional>
<rng:optional><rng:attribute name="height"/></rng:optional>
<rng:optional><rng:attribute name="rowspan"/></rng:optional>
<rng:optional><rng:attribute name="align"/></rng:optional>
<rng:optional><rng:attribute name="selection"/></rng:optional>
<rng:optional><rng:attribute name="default_focus"/></rng:optional>
<rng:optional><rng:attribute name="filters"/></rng:optional>
<rng:optional><rng:attribute name="statusbar_visible"/></rng:optional>
<rng:optional><rng:attribute name="can_create" /></rng:optional>
<rng:optional><rng:attribute name="can_write" /></rng:optional>
<rng:optional><rng:attribute name="interval" /></rng:optional>
<rng:optional><rng:attribute name="avatar_field" /></rng:optional>
<rng:optional><rng:attribute name="write_model" /></rng:optional>
<rng:optional><rng:attribute name="write_field" /></rng:optional>
<rng:optional><rng:attribute name="filter_field" /></rng:optional>
<rng:optional><rng:attribute name="text" /></rng:optional>
<rng:optional><rng:attribute name="optional" /></rng:optional>
<rng:optional><rng:attribute name="add-label"/></rng:optional>
<rng:optional><rng:attribute name="decoration-bf"/></rng:optional>
<rng:optional><rng:attribute name="decoration-it"/></rng:optional>
<rng:optional><rng:attribute name="decoration-danger"/></rng:optional>
<rng:optional><rng:attribute name="decoration-info"/></rng:optional>
<rng:optional><rng:attribute name="decoration-muted"/></rng:optional>
<rng:optional><rng:attribute name="decoration-primary"/></rng:optional>
<rng:optional><rng:attribute name="decoration-success"/></rng:optional>
<rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
<rng:optional><rng:attribute name="decoration-black"/></rng:optional>
<rng:optional><rng:attribute name="decoration-white"/></rng:optional>
<rng:optional><rng:attribute name="bg-danger"/></rng:optional>
<rng:optional><rng:attribute name="bg-info"/></rng:optional>
<rng:optional><rng:attribute name="bg-muted"/></rng:optional>
<rng:optional><rng:attribute name="bg-primary"/></rng:optional>
<rng:optional><rng:attribute name="bg-success"/></rng:optional>
<rng:optional><rng:attribute name="bg-warning"/></rng:optional>
<rng:optional><rng:attribute name="bg-black"/></rng:optional>
<rng:optional><rng:attribute name="bg-white"/></rng:optional>
<rng:optional><rng:attribute name="kanban_view_ref" /></rng:optional>
<rng:optional>
<rng:attribute name="force_save">
<rng:choice>
<rng:value>1</rng:value>
<rng:value>0</rng:value>
</rng:choice>
</rng:attribute>
</rng:optional>
<!-- Widget *static* options defined as an arbitrary JSON dict, with
widget-dependent parameters. To be ignored if widget/client does
not support them. -->
<rng:optional><rng:attribute name="options"/></rng:optional>
<rng:optional><rng:attribute name="placeholder"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="data"/>
<rng:ref name="field"/>
<rng:ref name="label"/>
<rng:ref name="separator"/>
<rng:ref name="xpath"/>
<rng:ref name="button"/>
<rng:ref name="group"/>
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="group">
<rng:element name="group">
<rng:ref name="overload"/>
<rng:ref name="access_rights"/>
<rng:optional><rng:attribute name="colspan"/></rng:optional>
<rng:optional><rng:attribute name="rowspan"/></rng:optional>
<rng:optional><rng:attribute name="expand"/></rng:optional>
<rng:optional><rng:attribute name="string"/></rng:optional>
<rng:optional><rng:attribute name="fill"/></rng:optional>
<rng:optional><rng:attribute name="height"/></rng:optional>
<rng:optional><rng:attribute name="width"/></rng:optional>
<rng:optional><rng:attribute name="name"/></rng:optional>
<rng:optional><rng:attribute name="color" /></rng:optional>
<rng:optional><rng:attribute name="invisible"/></rng:optional>
<rng:zeroOrMore>
<rng:ref name="field"/>
</rng:zeroOrMore>
<rng:ref name="container"/>
</rng:element>
</rng:define>
<rng:define name="button">
<rng:element name="button">
<rng:ref name="overload"/>
<rng:ref name="access_rights"/>
<rng:optional><rng:attribute name="invisible"/></rng:optional>
<rng:optional><rng:attribute name="column_invisible"/></rng:optional>
<rng:optional><rng:attribute name="disabled"/></rng:optional>
<rng:optional><rng:attribute name="name" /></rng:optional>
<rng:optional><rng:attribute name="icon" /></rng:optional>
<rng:optional><rng:attribute name="string" /></rng:optional>
<rng:optional><rng:attribute name="type" /></rng:optional>
<rng:optional><rng:attribute name="special" /></rng:optional>
<rng:optional><rng:attribute name="align" /></rng:optional>
<rng:optional><rng:attribute name="colspan"/></rng:optional>
<rng:optional><rng:attribute name="target"/></rng:optional>
<rng:optional><rng:attribute name="readonly"/></rng:optional>
<rng:optional><rng:attribute name="context"/></rng:optional>
<rng:optional><rng:attribute name="confirm"/></rng:optional>
<rng:optional><rng:attribute name="help"/></rng:optional>
<rng:optional><rng:attribute name="class"/></rng:optional>
<rng:optional><rng:attribute name="default_focus"/></rng:optional>
<rng:optional><rng:attribute name="tabindex"/></rng:optional>
<rng:optional><rng:attribute name="title"/></rng:optional>
<rng:optional><rng:attribute name="aria-label"/></rng:optional>
<rng:optional><rng:attribute name="aria-pressed"/></rng:optional>
<rng:optional><rng:attribute name="display"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="field" />
<rng:ref name="xpath" />
<rng:ref name="separator"/>
<rng:ref name="button"/>
<rng:ref name="group"/>
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="filter">
<rng:element name="filter">
<rng:ref name="overload"/>
<rng:ref name="access_rights"/>
<rng:attribute name="name"/>
<rng:optional><rng:attribute name="icon"/></rng:optional>
<rng:optional><rng:attribute name="invisible"/></rng:optional>
<rng:optional><rng:attribute name="separator" /></rng:optional>
<rng:optional><rng:attribute name="string" /></rng:optional>
<rng:optional><rng:attribute name="type" /></rng:optional>
<rng:optional><rng:attribute name="align" /></rng:optional>
<rng:optional><rng:attribute name="colspan"/></rng:optional>
<rng:optional><rng:attribute name="readonly"/></rng:optional>
<rng:optional><rng:attribute name="context"/></rng:optional>
<rng:optional><rng:attribute name="help"/></rng:optional>
<rng:optional><rng:attribute name="domain"/></rng:optional>
<rng:optional><rng:attribute name="date"/></rng:optional>
<rng:optional><rng:attribute name="default_period"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="field" />
<rng:ref name="xpath" />
<rng:ref name="separator"/>
<rng:ref name="button"/>
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="create">
<rng:element name="create">
<rng:ref name="overload"/>
<rng:attribute name="string"/>
<rng:optional><rng:attribute name="context"/></rng:optional>
<rng:optional><rng:attribute name="name"/></rng:optional>
</rng:element>
</rng:define>
<rng:define name="control">
<rng:element name="control">
<rng:ref name="overload"/>
<rng:oneOrMore>
<rng:choice>
<rng:ref name="create"/>
<rng:ref name="button"/>
</rng:choice>
</rng:oneOrMore>
</rng:element>
</rng:define>
<rng:define name="widget">
<rng:element name="widget">
<rng:attribute name="name"/>
<rng:optional><rng:attribute name="options"/></rng:optional>
</rng:element>
</rng:define>
</rng:grammar>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/annotation/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<!-- Handling of element overloading when inheriting from a base
template
-->
<rng:include href="common.rng"/>
<rng:define name="searchpanel">
<rng:element name="searchpanel">
<rng:ref name="overload"/>
<rng:optional><rng:attribute name="view_types"/></rng:optional>
<rng:optional><rng:attribute name="class"/></rng:optional>
<rng:optional><rng:attribute name="options"/></rng:optional>
<rng:optional><rng:attribute name="position"/></rng:optional>
<rng:zeroOrMore>
<rng:ref name="field" />
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="search">
<rng:element name="search">
<rng:ref name="overload"/>
<rng:optional><rng:attribute name="string"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="field"/>
<rng:ref name="group"/>
<rng:ref name="separator"/>
<rng:ref name="filter"/>
<rng:element name="newline"><rng:empty/></rng:element>
<rng:optional><rng:attribute name="options"/></rng:optional>
<rng:ref name="searchpanel"/>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:start>
<rng:choice>
<rng:ref name="search" />
</rng:choice>
</rng:start>
</rng:grammar>

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/annotation/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<!-- Handling of element overloading when inheriting from a base
template
-->
<rng:include href="common.rng"/>
<rng:define name="groupby">
<rng:element name="groupby">
<rng:attribute name="name"/>
<rng:optional><rng:attribute name="expand"/></rng:optional>
<rng:zeroOrMore>
<rng:ref name="field"/>
</rng:zeroOrMore>
<rng:zeroOrMore>
<rng:ref name="button"/>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="tree">
<rng:element name="tree">
<rng:ref name="overload"/>
<rng:optional><rng:attribute name="name"/></rng:optional>
<rng:optional><rng:attribute name="create"/></rng:optional>
<rng:optional><rng:attribute name="delete"/></rng:optional>
<rng:optional><rng:attribute name="edit"/></rng:optional>
<rng:optional><rng:attribute name="multi_edit"/></rng:optional>
<rng:optional><rng:attribute name="export_xlsx"/></rng:optional>
<rng:optional><rng:attribute name="duplicate"/></rng:optional>
<rng:optional><rng:attribute name="import"/></rng:optional>
<rng:optional><rng:attribute name="string"/></rng:optional> <!-- deprecated, has no effect anymore -->
<rng:optional><rng:attribute name="class"/></rng:optional>
<!-- Allows to take a custom View widget for handling -->
<rng:optional><rng:attribute name="js_class"/></rng:optional>
<rng:optional><rng:attribute name="default_order"/></rng:optional>
<rng:optional><rng:attribute name="default_group_by"/></rng:optional>
<rng:optional><rng:attribute name="decoration-bf"/></rng:optional>
<rng:optional><rng:attribute name="decoration-it"/></rng:optional>
<rng:optional><rng:attribute name="decoration-danger"/></rng:optional>
<rng:optional><rng:attribute name="decoration-info"/></rng:optional>
<rng:optional><rng:attribute name="decoration-muted"/></rng:optional>
<rng:optional><rng:attribute name="decoration-primary"/></rng:optional>
<rng:optional><rng:attribute name="decoration-success"/></rng:optional>
<rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
<rng:optional><rng:attribute name="decoration-black"/></rng:optional>
<rng:optional><rng:attribute name="decoration-white"/></rng:optional>
<rng:optional><rng:attribute name="bg-danger"/></rng:optional>
<rng:optional><rng:attribute name="bg-info"/></rng:optional>
<rng:optional><rng:attribute name="bg-muted"/></rng:optional>
<rng:optional><rng:attribute name="bg-primary"/></rng:optional>
<rng:optional><rng:attribute name="bg-success"/></rng:optional>
<rng:optional><rng:attribute name="bg-warning"/></rng:optional>
<rng:optional><rng:attribute name="bg-black"/></rng:optional>
<rng:optional><rng:attribute name="bg-white"/></rng:optional>
<rng:optional><rng:attribute name="banner_route"/></rng:optional>
<rng:optional><rng:attribute name="sample"/></rng:optional>
<rng:optional><rng:attribute name="action"/></rng:optional>
<rng:optional><rng:attribute name="type"/></rng:optional>
<rng:optional><rng:attribute name="open_form_view"/></rng:optional>
<rng:optional>
<rng:attribute name="limit">
<rng:data type="int"/>
</rng:attribute>
</rng:optional>
<rng:optional>
<rng:attribute name="count_limit">
<rng:data type="int"/>
</rng:attribute>
</rng:optional>
<rng:optional>
<rng:attribute name="groups_limit">
<rng:data type="int"/>
</rng:attribute>
</rng:optional>
<rng:optional>
<rng:attribute name="editable">
<rng:choice>
<rng:value>top</rng:value>
<rng:value>bottom</rng:value>
</rng:choice>
</rng:attribute>
</rng:optional>
<rng:optional><rng:attribute name="expand"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:element name="header">
<rng:zeroOrMore>
<rng:ref name="button"/>
</rng:zeroOrMore>
</rng:element>
<rng:ref name="control"/>
<rng:ref name="field"/>
<rng:ref name="widget"/>
<rng:ref name="separator"/>
<rng:ref name="tree"/>
<rng:ref name="groupby"/>
<rng:ref name="button"/>
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:start>
<rng:choice>
<rng:ref name="tree" />
</rng:choice>
</rng:start>
</rng:grammar>

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,121 @@
<section class="oe_container container">
<div class="oe_row oe_spaced" >
<div class="row">
<h2 class="oe_slogan">odooAi Common Util and Tools</h2>
<h3 class="oe_slogan"> Network and media and base tools </h3>
<div class="oe_row">
<h3>Lastest update: v17.24.04.29</h3>
<div class="row">
<img class="oe_demo oe_screenshot img img-fluid" style="max-height: 100%;" src="banner.png">
</div>
<div class="oe_span12 oe_spaced">
<div class="alert alert-info" style="padding:8px;font-weight: 300; font-size: 20px;">
<i class="fa fa-hand-o-right"></i><b> Key features: </b>
<ul class="list-unstyled">
<li>
<i class="fa fa-check-square-o text-primary"></i>
1. Quick import data from excel with .py code
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
2. Quick m2o default value
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
3. Filter for useless field
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
4. UTC local timezone convert
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
5. Get browser ua, user-agent
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
6. Image to local, image url to local, media to local attachment
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
7. Log cron job
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
8. Boost for less no use mail
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
9. Customize .rng file
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
10. Misc like get distance between two points
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
11. Multi-language Support. Multi-Company Support
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
12. Support Odoo 17,16,15,14,13,12, Enterprise and Community and odoo.sh Edition.
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
11. Multi-language Support. Multi-Company Support
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
13. Full Open Source.
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="oe_container container">
<div class="oe_row oe_spaced">
<h2 class="bg-warning text-center pt8 pb8">Setup, please run the follow command to install the lib.</h2>
<h4 class="oe_slogan"> pip install pyyaml ua-parser user-agents </h4>
<h4 class="oe_slogan"> After that, install the app_common </h4>
</div>
</section>
<section class="container oe_dark">
<div class="oe_row oe_spaced text-center">
<div class="row">
<h2 class="oe_slogan">Technical Help & Support</h2>
</div>
<div class="col-md-12 pad0">
<div class="oe_mt16">
<p><h4>
For any type of technical help & support requests, Feel free to contact us</h4></p>
<a style="background: #002e5a none repeat scroll 0% 0%; color: rgb(255, 255, 255);position: relative; overflow: hidden;"
class="btn btn-warning btn-lg" rel="nofollow" href="mailto:odoo@china.com"><span
style="height: 354px; width: 354px; top: -147.433px; left: -6.93335px;" class="o_ripple"></span>
<i class="fa fa-envelope"></i> odoo@china.com</a>
<p><h4>
Via QQ: 300883 (App user would not get QQ or any other IM support. Only for odoo project customize.)</h4></p>
<a style="background: #002e5a none repeat scroll 0% 0%; color: rgb(255, 255, 255);position: relative; overflow: hidden;"
class="btn btn-warning btn-lg" rel="nofollow" href="mailto:300883@qq.com"><span
style="height: 354px; width: 354px; top: -147.433px; left: -6.93335px;" class="o_ripple"></span>
<i class="fa fa-envelope"></i> 300883@qq.com</a>
</div>
<div class="oe_mt16">
<h4>
Visit our website for more support.</h4>
<h4>https://www.odooai.cn</h4>
</div>
</div>
</div>
<div class="oe_row oe_spaced text-center">
<h2>More Powerful addons, Make your odoo very easy to use, easy customize:
<a class="btn btn-primary mb16" href="http://www.odoo.com/apps/modules/browse?author=odooai.cn">odooai.cn Odoo Addons</a>
</h2>
</div>
</section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

View File

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<odoo>
<record id="app_ir_cron_view_tree" model="ir.ui.view">
<field name="name">app.ir.cron.tree</field>
<field name="model">ir.cron</field>
<field name="inherit_id" ref="base.ir_cron_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='active']" position="before">
<field name='trigger_user_id' optional="show"/>
</xpath>
</field>
</record>
</odoo>

View File