update app to 15

This commit is contained in:
ivan deng
2021-12-03 14:20:22 +08:00
parent dbaa87081a
commit 627381c1be
37 changed files with 10201 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,84 @@
# -*- coding: utf-8 -*-
# Created on 20120-01-05
# author: 广州尚鹏https://www.sunpop.cn
# email: 300883@qq.com
# resource of Sunpop
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
# Odoo12在线用户手册长期更新
# https://www.sunpop.cn/documentation/user/12.0/zh_CN/index.html
# Odoo12在线开发者手册长期更新
# https://www.sunpop.cn/documentation/12.0/index.html
# Odoo10在线中文用户手册长期更新
# https://www.sunpop.cn/documentation/user/10.0/zh_CN/index.html
# Odoo10离线中文用户手册下载
# https://www.sunpop.cn/odoo10_user_manual_document_offline/
# Odoo10离线开发手册下载-含python教程jquery参考Jinja2模板PostgresSQL参考odoo开发必备
# https://www.sunpop.cn/odoo10_developer_document_offline/
##############################################################################
# Copyright (C) 2009-TODAY Sunpop.cn Ltd. https://www.sunpop.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': "Sunpop Odooapp Common Func",
'version': '15.21.11.30',
'author': 'Sunpop.cn',
'category': 'Base',
'website': 'https://www.sunpop.cn',
'license': 'LGPL-3',
'sequence': 2,
'price': 0.00,
'currency': 'EUR',
'images': ['static/description/banner.png'],
'summary': '''
Core for common use sunpop apps.
基础核心必须没有要被依赖字段及视图等实现auto_install
''',
'description': '''
Support Odoo 15, Enterprise and Community Edition
1.
2.
3. Multi-language Support.
4. Multi-Company Support.
5. Support Odoo 15, Enterprise and Community Edition
==========
1.
2.
3. 多语言支持
4. 多公司支持
5. Odoo 15, 企业版,社区版,多版本支持
''',
'depends': [
'base',
'web',
],
'data': [
# 'security/*.xml',
# 'security/ir.model.access.csv',
# 'data/.xml',
'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',
'installable': True,
'application': True,
'auto_install': True,
}

View File

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

View File

@@ -0,0 +1,95 @@
# -*- 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 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('/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 = self.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):
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
# 微信浏览器开发工具小程序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
utype = 'web'
# todo: 引入现成 py lib处理企业微信
if 'MicroMessenger' in ua and 'webdebugger' not in ua and ('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
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.sunpop.cn
# email: 300883@qq.com
# resource of Sunpop
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
# Odoo在线中文用户手册长期更新
# https://www.sunpop.cn/documentation/user/10.0/zh_CN/index.html
# Odoo10离线中文用户手册下载
# https://www.sunpop.cn/odoo10_user_manual_document_offline/
# Odoo10离线开发手册下载-含python教程jquery参考Jinja2模板PostgresSQL参考odoo开发必备
# https://www.sunpop.cn/odoo10_developer_document_offline/
# description:
from odoo import api, SUPERUSER_ID, _
def pre_init_hook(cr):
pass
# cr.execute("")
def post_init_hook(cr, registry):
pass
# cr.execute("")
def uninstall_hook(cr, registry):
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,30 @@
# -*- coding: utf-8 -*-
# Part of Sunpop.cn. See LICENSE file for full copyright and licensing details.
# Created on 2019-04-20
# author: 广州尚鹏http://www.sunpop.cn
# email: 300883@qq.com
# resource of Sunpop
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
# Odoo12在线用户手册长期更新
# http://www.sunpop.cn/documentation/user/12.0/en/index.html
# Odoo12在线开发者手册长期更新
# http://www.sunpop.cn/documentation/12.0/index.html
# Odoo10在线中文用户手册长期更新
# http://www.sunpop.cn/documentation/user/10.0/zh_CN/index.html
# Odoo10离线中文用户手册下载
# http://www.sunpop.cn/odoo10_user_manual_document_offline/
# Odoo10离线开发手册下载-含python教程jquery参考Jinja2模板PostgresSQL参考odoo开发必备
# http://www.sunpop.cn/odoo10_developer_document_offline/
# description:
from . import base
from . import ir_ui_view
from . import ir_cron
from . import res_users

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

@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
import requests
import base64
from io import BytesIO
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 _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:
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):
if not url:
return None
try:
response = requests.get(url) # 将这个图片保存在内存
except Exception as e:
return None
# 返回这个图片的base64编码
return base64.b64encode(BytesIO(response.content).read())

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,41 @@
# -*- 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('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, *args, **kwargs):
super(View, self).__init__(*args, **kwargs)
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,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 -*-

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

@@ -0,0 +1,433 @@
<?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="modifiable">
<rng:optional>
<!-- @modifiers contains a JSON map unifying the various
modifier attributes: @readonly, @required, @invisible.
Each attribute is a key, mapped to a JSON list representing
a condition expressed as an OpenERP `domain` filter
Only some of the modifier keys make sense on some
elements, for example <filter> and <group> only support
`invisible`. -->
<rng:attribute name="modifiers"/>
</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:ref name="modifiable"/>
<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:ref name="modifiable"/>
<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:ref name="modifiable"/>
<rng:optional><rng:attribute name="allow_group_range_value"/></rng:optional>
<rng:optional><rng:attribute name="domain_filter"/></rng:optional>
<rng:optional><rng:attribute name="attrs"/></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="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="states"/></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="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:ref name="modifiable"/>
<rng:optional><rng:attribute name="attrs"/></rng:optional>
<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="states"/></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:ref name="modifiable"/>
<rng:optional><rng:attribute name="attrs"/></rng:optional>
<rng:optional><rng:attribute name="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="states" /></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: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:ref name="modifiable"/>
<rng:attribute name="name"/>
<rng:optional><rng:attribute name="attrs"/></rng:optional>
<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:attribute name="context"/>
</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:choice>
</rng:oneOrMore>
</rng:element>
</rng:define>
</rng:grammar>

View File

@@ -0,0 +1,44 @@
<?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: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,105 @@
<?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="multi_group_select"/></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="options"/></rng:optional>
<rng:optional><rng:attribute name="default_order"/></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="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="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>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- model权限 -->
<record id="acc_module" model="ir.model.access">
<field name="name">acc_module_user</field>
<field name="model_id" ref="model_app_order"/>
<field name="group_id" ref="base.group_user"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
<!-- 应用权限目录 -->
<record model="ir.module.category" id="app_module_category_1">
<field name="name">App...</field>
<field name="description">Helps you manage your ...</field>
<field name="sequence">8</field>
</record>
<!-- 权限用户 -->
<!-- 普通 -->
<record id="group_app_user" model="res.groups">
<field name="name">App User</field>
<field name="category_id" ref="app_module_category_1"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="comment">The user will be able to ...</field>
</record>
<!-- 管理员,可以配置 -->
<record id="group_app_admin" model="res.groups">
<field name="name">App Admin</field>
<field name="category_id" ref="app_module_category_1"/>
<field name="implied_ids" eval="[(4, ref('group_app_user'))]"/>
<field name="comment">The user will be able to config ...</field>
</record>
<!-- group_erp_manager自动有完整权限 -->
<record id="base.group_erp_manager" model="res.groups">
<field name="implied_ids" eval="[(4, ref('group_app_admin'))]"/>
</record>
<!-- Rules -->
<record id="rule_user" model="ir.rule">
<field name="name">Users are allowed to access their own m///</field>
<field name="model_id" ref="model_app_order"/>
<field name="domain_force">['|', ('partner_id', 'in', [user.partner_id.id]), ('user_id.id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<!--End -->
</data>
</odoo>

View File

@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_product_attribute_value_group_user,product_attribute_value_group.user,app_product_variant_pro.model_product_attribute_value_group,base.group_user,1,0,0,0
access_product_attribute_value_group_manager,product_attribute_value_group.manager,app_product_variant_pro.model_product_attribute_value_group,base.group_erp_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_product_attribute_value_group_user product_attribute_value_group.user app_product_variant_pro.model_product_attribute_value_group base.group_user 1 0 0 0
3 access_product_attribute_value_group_manager product_attribute_value_group.manager app_product_variant_pro.model_product_attribute_value_group base.group_erp_manager 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,103 @@
<section class="oe_container">
<div class="oe_row oe_spaced" >
<div class="oe_span12">
<h2 class="oe_slogan"> </h2>
<h3 class="oe_slogan"> </h3>
<div class="oe_row">
<h3>Lastest update: v15.21.11.30</h3>
<div class="oe_span12">
<img class="oe_demo oe_screenshot" 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>
Put key function here.
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
3. Multi-language Support.
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
4. Multi-Company Support.
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
5. Support Odoo 15, 14, 13, 12, 11, Enterprise and Community Edition.
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h1 class="text-danger text-center">Setup, please run the follow command to install the lib.</h1>
<h4 class="oe_slogan"> pip install pyyaml ua-parser user-agents </h4>
<div class="oe_demo oe_screenshot">
<img src=".jpg"/>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h1 class="text-danger text-center">So Easy to navigator and search any data.</h1>
<h4 class="oe_slogan"> </h4>
<div class="oe_demo oe_screenshot">
<img src=".jpg"/>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h1 class="text-danger text-center">Multi-language Support..</h1>
<h4 class="oe_slogan"> </h4>
<div class="oe_demo oe_screenshot">
<img src="cnreadme.jpg"/>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced text-center">
<div class="oe_span12">
<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:guohuadeng@hotmail.com"><span
style="height: 354px; width: 354px; top: -147.433px; left: -6.93335px;" class="o_ripple"></span>
<i class="fa fa-envelope"></i> guohuadeng@hotmail.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.sunpop.cn</h4>
</div>
</div>
</div>
<div class="oe_row oe_spaced text-center">
<h1>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=Sunpop.cn">Supop.cn Odoo Addons</a>
</h1>
</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

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="app_product_brand_search_view" model="ir.ui.view">
<field name="name">product.brand.search</field>
<field name="model">product.brand</field>
<field name="arch" type="xml">
<search string="Product Brand">
<field name="name"/>
<field name="partner_id"/>
</search>
</field>
</record>
<act_window
id="action_open_brand_products"
name="Brand Products"
res_model="product.template"
view_mode="kanban,form,tree"
domain="[('product_brand_id', '=', active_id)]"/>
<act_window
id="action_open_single_product_brand"
name="Product Brand"
res_model="product.brand"
view_mode="kanban,form,tree"
target="current"
domain="[('product_ids', 'in', active_id)]"/>
<record id="app_product_brand_form_view" model="ir.ui.view">
<field name="name">product.brand.form</field>
<field name="model">product.brand</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(action_open_brand_products)d"
type="action"
class="oe_stat_button"
icon="fa-cubes">
<field name="products_count" widget="statinfo" string="Products"/>
</button>
</div>
<field name="logo" widget="image" class="oe_avatar"/>
<div class="oe_title">
<label for="name" string="Brand Name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="partner_id"/>
</group>
</group>
<group string="Description">
<field name="description" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="product_brand_tree_view" model="ir.ui.view">
<field name="name">product.brand.tree</field>
<field name="model">product.brand</field>
<field name="arch" type="xml">
<tree string="product.brand">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="description"/>
<field name="partner_id"/>
</tree>
</field>
</record>
<record id="product_brand_kanban_view" model="ir.ui.view">
<field name="name">product.brand.kanban</field>
<field name="model">product.brand</field>
<field name="arch" type="xml">
<kanban>
<field name="id"/>
<field name="logo"/>
<field name="products_count"/>
<field name="description"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_image">
<img t-att-src="kanban_image('product.brand', 'logo', record.id.raw_value)" alt="Logo"/>
</div>
<div class="oe_kanban_details">
<h4>
<field name="name"/>
</h4>
<div>
<a name="%(action_open_brand_products)d" type="action">
<t t-esc="record.products_count.value"/> Products
</a>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="action_product_brand">
<field name="name">Brand</field>
<field name="res_model">product.brand</field>
<field name="view_mode">kanban,form,tree</field>
</record>
<menuitem id="menu_product_brand" name="Product Brands"
action="action_product_brand"
sequence="8"
parent="stock.menu_product_in_config_stock"/>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!--产品list可调整显示顺序-->
<record id="app_product_product_tree_view" model="ir.ui.view">
<field name="name">app.product.product.tree</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_product_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='type']" position="after">
<field name="abc_classification" widget="label_selection"
options="{'classes': {'a': 'danger', 'b': 'primary', 'c': 'default'}}"/>
</xpath>
</field>
</record>
<record id="product_product_action" model="ir.actions.act_window">
<field name="name">Product Manager</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.product</field>
<field name="view_mode">tree,form</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_product_template_search_brand" model="ir.ui.view">
<field name="name">product.template.search.brand</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="product_brand_id"/>
<filter string="Brand" name="groupby_brand" domain="[]"
context="{'group_by': 'product_brand_id'}"/>
<separator/>
</field>
</field>
</record>
<record id="product_template_form_brand_add" model="ir.ui.view">
<field name="name">product.template.product.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<field name="sale_ok" position="before">
<field name="product_brand_id" placeholder="Brand"/>
<div/>
</field>
</field>
</record>
<record id="view_product_template_kanban_brand" model="ir.ui.view">
<field name="name">product kanban view add brand</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_kanban_view"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_kanban_details')]/strong[1]" position="after">
<div>
<a t-if="record.product_brand_id" type="action"
name="%(action_open_single_product_brand)d">
<field name="product_brand_id"/>
</a>
</div>
</xpath>
</field>
</record>
<record id="view_product_template_tree_brand" model="ir.ui.view">
<field name="name">product tree view add brand</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_tree_view"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="product_brand_id"/>
</field>
</field>
</record>
</data>
</odoo>

View File