企业微信 模块

This commit is contained in:
ljmwkx
2024-06-30 09:08:39 +08:00
parent 76a477ae3b
commit c5dfd26014
209 changed files with 0 additions and 21583 deletions
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from . import models
from . import controllers
-43
View File
@@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
{
"name": "iErp Web Widgets",
"author": "RStudio",
"category": "iErp Suites/Widget",
"summary": "iErp Widgets",
"website": "https://eis-solution.coding.net/public/odoo/oec/git",
"version": "17.0.0.1",
"description": """
""",
"depends": [
"web",
"attachment_indexation",
],
"external_dependencies": {
"python": [
# "pyzbar",
],
},
"data": [],
"assets": {
# "web.assets_common": [
# "web_widgets/static/fonts/fonts.scss",
# ],
"web.assets_backend": [
"web_widgets/static/fonts/fonts.scss",
"web_widgets/static/src/views/**/*.js",
"web_widgets/static/src/views/**/*.xml",
"web_widgets/static/src/views/**/*.scss",
"web_widgets/static/src/webclient/**/*",
],
"web.assets_frontend": [
# "web_widgets/static/lib/officetohtml/**/*",
],
},
"sequence": 500,
"installable": True,
"auto_install": True,
"application": False,
"license": "AGPL-3",
}
-3
View File
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import binary
-61
View File
@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
import os
import logging
from odoo import SUPERUSER_ID, _, http
from odoo.exceptions import AccessError, MissingError, ValidationError
from odoo.fields import Command
from odoo.http import request
from odoo.addons.web.controllers.binary import Binary
from odoo.exceptions import AccessError, UserError
from odoo.tools import file_open, file_path, replace_exceptions
_logger = logging.getLogger(__name__)
class WebWidgetBinary(Binary):
@http.route(
["/<string:model>/<string:field>/<int:id>.<string:filetype>"],
type="http",
# auth="public",
auth="user",
)
def get_office_url(
self,
xmlid=None,
model="ir.attachment",
id=None,
field="raw",
filetype=None,
filename=None,
filename_field="name",
mimetype=None,
unique=False,
download=False,
access_token=None,
nocache=False,
):
print(model, field, id, filename, filename_field, filetype)
with replace_exceptions(UserError, by=request.not_found()):
record = request.env["ir.binary"]._find_record(
xmlid, model, id and int(id), access_token
)
stream = request.env["ir.binary"]._get_stream_from(
record, field, filename, filename_field, mimetype
)
send_file_kwargs = {"as_attachment": False}
if unique:
send_file_kwargs["immutable"] = True
send_file_kwargs["max_age"] = http.STATIC_CACHE_LONG
if nocache:
send_file_kwargs["max_age"] = None
res = stream.get_response(**send_file_kwargs)
res.headers["Content-Security-Policy"] = "default-src 'none'"
return "https://wecom.rstudio.xyz/web_widgets/static/files/office/sale_order_outsourcing/finalized_office_file_4.docx"
return res
-93
View File
@@ -1,93 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * web_widgets
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-10 04:17+0000\n"
"PO-Revision-Date: 2024-03-10 12:19+0800\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.4.2\n"
#. module: web_widgets
#. odoo-javascript
#: code:addons/web_widgets/static/src/webclient/actions/api_error_dialog/api_error_dialog.js:0
#, python-format
msgid "API Error"
msgstr "API错误"
#. module: web_widgets
#. odoo-javascript
#: code:addons/web_widgets/static/src/webclient/actions/api_error_dialog/api_error_dialog.xml:0
#, python-format
msgid "Error Details:"
msgstr "错误详细信息:"
#. module: web_widgets
#. odoo-javascript
#: code:addons/web_widgets/static/src/webclient/actions/api_error_dialog/api_error_dialog.xml:0
#, python-format
msgid "Error code:"
msgstr "错误代码:"
#. module: web_widgets
#. odoo-javascript
#: code:addons/web_widgets/static/src/webclient/actions/api_error_dialog/api_error_dialog.xml:0
#, python-format
msgid "Error description:"
msgstr "错误描述:"
#. module: web_widgets
#. odoo-javascript
#: code:addons/web_widgets/static/src/webclient/actions/api_error_dialog/api_error_dialog.xml:0
#, python-format
msgid "Error troubleshooting method:"
msgstr "错误排除方法:"
#. module: web_widgets
#. odoo-python
#: code:addons/web_widgets/models/ir_module.py:0
#, python-format
msgid "Failed to upload file %s, reason:%s"
msgstr "无法上载文件%s,原因:%s"
#. module: web_widgets
#: model:ir.model,name:web_widgets.model_ir_module_module
msgid "Module"
msgstr "模块"
#. module: web_widgets
#. odoo-javascript
#: code:addons/web_widgets/static/src/views/fields/password_char/password_char.js:0
#, python-format
msgid "String decrypted"
msgstr ""
#. module: web_widgets
#. odoo-javascript
#: code:addons/web_widgets/static/src/views/fields/password_char/password_char.js:0
#, python-format
msgid "String encrypted"
msgstr ""
#. module: web_widgets
#. odoo-python
#: code:addons/web_widgets/models/ir_module.py:0
#, python-format
msgid "Successfully uploaded %s"
msgstr "已成功上传 %s"
#. module: web_widgets
#. odoo-javascript
#: code:addons/web_widgets/static/src/views/fields/password_char/password_char.js:0
#, python-format
msgid "Text is displayed in password characters"
msgstr "文本以密码字符显示"
-3
View File
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import ir_module
-37
View File
@@ -1,37 +0,0 @@
# -*- coding: utf-8 -*-
import os
import platform
import base64
import pathlib
from datetime import datetime, timedelta
from odoo import api, fields, models, tools, _
from odoo.tools.misc import file_path
import logging
_logger = logging.getLogger(__name__)
class Module(models.Model):
_inherit = "ir.module.module"
@api.model
def get_module_installation_status(self, addon_name=None):
"""
获取 addon 是否安装
"""
addon = self.sudo().search([("name", "=", addon_name)])
if not addon:
# return {
# "exist": False,
# "moduleId": 0,
# }
return False
else:
# addon.button_install()
# return {
# "exist": True,
# "moduleId": addon.id,
# }
return True
-106
View File
@@ -1,106 +0,0 @@
# 企微 Widget
## Json编辑器
创建一个 `fields.Json` 的字段,然后在视图中使用 `json_editor` 的 `widget`,即可使用 `json` 编辑器
```python
report_data_set = fields.Json(string="Report Data Set", translate=False, help="Report Data Set.")
```
```xml
<field name="report_data_set" widget="json_editor" options="{'mode': 'code', 'modes':['code', 'form', 'text', 'tree', 'view', 'preview']}" force_save="1" readonly="1"/>
```
> `options` 参数说明:
| 选项| 说明 |
|----------|-----:|
|mode| 默认模式 |
|modes| 模式清单,可选有: `code`, `form`, `text`, `tree`, `view`, `preview`|
## Markdown 编辑器
### 使用
```xml
<field name="description" widget="markdown_editor" options="{'height': 'auto','theme': 'dark', 'previewStyle':'vertical', }"/>
```
> `options` 参数说明:
| 选项| 类型 |说明 |
|----------|-----:|-----:|
|height|字符串 |高度 例如 `300px` 或者 `auto`|
|theme|字符串 |主题 `dark` 或者为 空 |
|previewStyle| 字符串|预览样式,可选有: 选项卡 `tab`, 垂直 `vertical`|
|userPlugin| 布尔值|使用组件,默认为`false`|
> 组件列表说明:
| 名称| 说明 |链接 |
|----------|-----:|-----:|
|chart|图表插件,`测试无法正常使用`| <a href="https://nhn.github.io/tui.editor/latest/tutorial-example07-editor-with-chart-plugin" target="_blank">打开链接</a>|
|code-syntax-highlight|代码语法突出显示插件,`可以正常使用`| <a href="https://nhn.github.io/tui.editor/latest/tutorial-example08-editor-with-code-syntax-highlight-plugin" target="_blank">打开链接</a>|
|colorSyntax|颜色语法插件,`测试无法正常使用`| <a href="https://nhn.github.io/tui.editor/latest/tutorial-example09-editor-with-color-syntax-plugin" target="_blank">打开链接</a>|
|tableMergedCell|表格合并单元格插件,`测试无法正常使用`| <a href="https://nhn.github.io/tui.editor/latest/tutorial-example10-editor-with-table-merged-cell-plugin" target="_blank">打开链接</a>|
|uml|UML 插件,`测试无法正常使用`| <a href="https://nhn.github.io/tui.editor/latest/tutorial-example11-editor-with-uml-plugin" target="_blank">打开链接</a>|
## 扫码
1. 支持条码及二维码
2. 支持同时扫码多个条码及二维码
3. 调用 `企业微信扫一扫` 仅支持单个条码及二维码
### `ScanCodeChar`
```xml
<field name="barcode" widget="ScanCodeChar" options="{'need_confirm': true, 'autoplay':true}" />
```
### `ScanCodeText`
```xml
<field name="barcode" widget="ScanCodeText" options="{'need_confirm': true, 'autoplay':true}" />
```
### `ScanCodeURL`
```xml
<field name="barcode" widget="ScanCodeURL" options="{'need_confirm': true, 'autoplay':true}" />
```
## 密码显示 `display_password`
```xml
<field name="contacts_secret" widget="password_char" require_encryption="True" />
```
## 一对多配置 one2many_config
```xml
<field name="contacts_app_config_ids" widget="one2many_config" format="value" type="ttype" help="description" class="w-100 pl-3" t-translation="off">
<tree create="0" edit="true" delete="0" editable="bottom" sample="1">
<field name="name" readonly="1"/>
<field name="key" readonly="1"/>
<field name="value" need_format="true" force_save="1" required="1"/>
<field name="description" invisible="1"/>
</tree>
</field>
```
## 从 form 视图 返回 list 视图,模拟 alt+b 的组合键
> 应一个客户的需求开发
### 在form 视图添加一个按钮,如下:
```html
<button name="action_confirm" id="action_confirm" string="Confirm" class="btn-primary" type="object" icon="fa-check" invisible="state not in ['draft','error']"/>
```
### 此按钮python 代码如下:
```python
def action_confirm(self):
# 省略一万字的代码字符串
action = {
"type": "ir.actions.client",
"tag": "open_list_action",
}
return action
```
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

-4
View File
@@ -1,4 +0,0 @@
@font-face {
font-family: 'password';
src: url('/web_widgets/static/fonts/password.ttf');
}
Binary file not shown.
@@ -1,13 +0,0 @@
.o_field_widget {
// MS Office Viewer
&.o_field_ms_office_viewer {
.o_msoffice_view_iframe {
width: 100%;
height: 450px;
border: 0;
margin-top: 10px;
}
}
}
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.PasswordCharField" owl="1">
<t t-if="props.readonly">
<span t-esc="props.record.data[props.name] or ''" />
<!-- <span t-esc="formattedValue" /> -->
</t>
<t t-else="">
<!-- <input type="text" class="o_input" autocomplete="off" t-att-id="props.id" t-att-placeholder="props.placeholder" t-att-data-encrypted="`${state.encrypted}`" t-att-required="props.required" t-ref="input" t-on-input="onInput"/> -->
<input type="text" class="o_input" autocomplete="off" t-att-id="props.id" t-att-placeholder="props.placeholder" data-encrypted="true" t-att-required="props.required" t-ref="input" />
<!-- <button type="button" t-ref="button" t-attf-class="btn btn-sm btn-primary o_display_password_button" t-on-click.stop="(event) => onClick(event.target,state.encrypted)">
<i t-attf-class="fa fa-eye{{state.encrypted ? '-slash':''}}"/>
</button> -->
<button type="button" t-ref="button" t-attf-class="btn btn-sm btn-primary o_display_password_button" t-on-click.stop="() => this.onClick()">
<i t-attf-class="fa fa-eye{{state.encrypted ? '-slash':''}}"/>
</button>
</t>
</t>
</templates>
@@ -1,103 +0,0 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { useInputField } from "@web/views/fields/input_field_hook";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { useService } from "@web/core/utils/hooks";
const { Component, useState, useRef, onMounted, onRendered, onPatched } = owl;
// --------------------------------------------------------------------------------
// DisplayPasswordChar
// --------------------------------------------------------------------------------
export class PasswordCharField extends Component {
static template = "web.PasswordCharField";
static props = {
...standardFieldProps,
string: { type: String, optional: true },
className: { type: String, optional: true },
placeholder: { type: String, optional: true },
shouldTrim: { type: Boolean, optional: true },
encrypted: { type: Boolean, optional: true },
};
setup() {
this.state = useState({
...this.state,
encrypted: true,
});
// this.encrypted = true; // 是否加密
this.input = useRef("input");
this.button = useRef("button");
this.popover = useService("popover");
useInputField({
getValue: () => this.props.record.data[this.props.name] || "",
refName: "input",
// parse: (v) => this.parse(v),
});
onMounted(() => {
// console.log("-------onMounted", this.input.el)
if (this.props.record.data[this.props.name].trim() != "" && this.input.el) {
this.input.el.dataset.encrypted = "true";
} else if (this.props.record.data[this.props.name].trim() == "" && this.input.el) {
this.input.el.dataset.encrypted = "false";
}
});
// onRendered(() => {
// // console.log("-------2", this.formattedValue)
// // if (this.props.record.data[this.props.name].trim() != "") {
// // // this.state.encrypted = true;
// // this.input.el.dataset.encrypted = "true";
// // } else {
// // // this.state.encrypted = false;
// // this.input.el.dataset.encrypted = "false";
// // }
// });
}
parse(value) {
if (this.shouldTrim) {
return value.trim();
}
return value;
}
// onClick(event, value) {
onClick() {
let icon = this.button.el.firstChild;
if (this.state.encrypted) {
// 当前状态为加密状态,点击后变为解密状态
icon.classList.remove("fa-eye");
icon.classList.add("fa-eye-slash");
this.input.el.dataset.encrypted = "false";
this.state.encrypted = false;
} else {
// 当前状态为解密状态,点击后变为加密状态
icon.classList.remove("fa-eye-slash");
icon.classList.add("fa-eye");
this.input.el.dataset.encrypted = "true";
this.state.encrypted = true;
}
}
}
export const passwordCharField = {
component: PasswordCharField,
displayName: _t("Text is displayed in password characters"),
supportedTypes: ["char"],
extractProps: ({ attrs }) => ({
placeholder: attrs.placeholder,
}),
};
registry.category("fields").add("password_char", passwordCharField);
@@ -1,51 +0,0 @@
.o_field_password_char {
width: 100% !important;
border: 1px solid $primary;
display: flex !important;
justify-content: space-between !important;
&[data-encrypted="true"] {
font-family: "password";
font-size: smaller;
}
span {
width: 100% !important;
}
span,
input {
&[data-encrypted="true"] {
font-family: "password";
font-size: smaller;
}
&[data-encrypted="false"] {
font-family: inherit;
font-size: inherit;
}
padding: 1px 0;
}
// input:empty {
// font-family: inherit;
// }
input::placeholde {
font-family: inherit;
}
button {
border-radius: 0;
}
.form-control {
&:focus {
// @include print-variable(o-input-border-color, $o-brand-primary);
box-shadow: none;
// background-color: #FFFFFF;
border-color: transparent;
}
}
}
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.PasswordCharField" owl="1">
<input type="text" class="o_input" autocomplete="off" t-att-id="props.id" t-att-placeholder="props.placeholder" data-encrypted="true" t-att-required="props.required" t-ref="input" t-att-readonly="props.readonly" />
<button type="button" t-ref="button" t-attf-class="btn btn-sm btn-primary o_display_password_button" t-on-click.stop="() => this.onClick()">
<i t-attf-class="fa fa-eye{{state.encrypted ? '-slash':''}}"/>
</button>
</t>
</templates>
@@ -1,26 +0,0 @@
/** @odoo-module **/
import { Dialog } from "@web/core/dialog/dialog";
import { _lt } from "@web/core/l10n/translation";
import { markup } from "@odoo/owl";
const { Component, } = owl;
export class ApiErrorDialog extends Component {
setup() {
this.title = this.props.title;
this.size = this.props.size;
this.code = this.props.code;
this.description = this.props.description;
this.method = markup(this.props.method);
this.details = this.props.details;
this.bodyClass = "bg-warning";
this.contentClass = "bg-warning";
}
}
ApiErrorDialog.components = {
Dialog
};
ApiErrorDialog.template = "web.ApiErrorDialog";
ApiErrorDialog.title = _lt("API Error");
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<template id="template" xml:space="preserve">
<t t-name="web.ApiErrorDialog" owl="1">
<Dialog size="size" contentClass="contentClass" bodyClass="bodyClass" title="this.constructor.title">
<p>
<b>Error code: </b>
<b t-esc="code"/>
</p>
<p>
<b>Error description:</b>
<b t-esc="description"/>
</p>
<p>
<b>Error troubleshooting method:</b>
<t t-out="method"/>
</p>
<p>
<b>Error Details:</b>
<t t-esc="details"/>
</p>
</Dialog>
</t>
</template>
@@ -1,12 +0,0 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { ApiErrorDialog } from "./api_error_dialog";
export function apiDialogAction(env, action) {
const params = action.params || {};
env.services.dialog.add(ApiErrorDialog, params, {});
}
registry.category("actions").add("api_error_dialog", apiDialogAction);
@@ -1,25 +0,0 @@
/** @odoo-module **/
import { registry } from '@web/core/registry';
export function openListAction(env, action) {
if (Object.keys(action).length > 0) {
window.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'b',
ctrlKey: false,
shiftKey: false,
altKey: true,
metaKey: false,
bubbles: true,
cancelable: true
})
);
} else {
return;
}
}
registry.category('actions').add('open_list_action', openListAction);
-19
View File
@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from odoo.tools import sql
from . import api
from . import tools
from . import models
from . import controllers
# from odoo import api, SUPERUSER_ID, _
# def post_init_hook(cr, registry):
# # 安装模块后清理 清除所有的记录,并且索引号从0开始
# env = api.Environment(cr, SUPERUSER_ID, {})
# query = """
# TRUNCATE TABLE wecom_app_config RESTART IDENTITY CASCADE;
# """
# env.cr.execute(query)
-46
View File
@@ -1,46 +0,0 @@
# -*- coding: utf-8 -*-
{
"name": "WeCom API",
"author": "RStudio",
"website": "https://gitee.com/rainbowstudio/wecom",
"sequence": 600,
"installable": True,
"application": False,
"auto_install": False,
"category": "WeCom Suites/Settings",
"version": "17.0.0.1",
"summary": """
WeCom Service-side API and Client-side API
""",
"description": """
WeCom Service-side API and Client-side API
Reconstruction based on project "https://github.com/sbzhu/weworkapi_python"
""",
"depends": [],
"data": [
"security/ir.model.access.csv",
"data/ir_config_parameter.xml",
"data/wecom_server_api_error_data.xml",
"data/service_api_list_data.xml",
],
"assets": {
"web.assets_common": [
# JS
# "wecom_api/static/src/js/*.js",
],
"web.assets_backend": [
# JS
],
},
"external_dependencies": {
"python": [
"requests_toolbelt",
# "pandas",
"xmltodict",
"pycryptodome",
"html2text",
],
},
"license": "AGPL-3",
# "post_init_hook": "post_init_hook",
}
-6
View File
@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
from . import wecom_abstract_api
from . import wecom_server_api
from . import wecom_message_api
from . import wecom_client_api
-198
View File
@@ -1,198 +0,0 @@
# -*- coding: utf-8 -*-
import json
import requests
from odoo import api, fields, models, _, SUPERUSER_ID
from odoo.exceptions import UserError
import warnings
class ApiException(Exception):
def __init__(self, errCode, errMsg):
super().__init__(errCode, errMsg)
self.errCode = errCode
self.errMsg = errMsg
class WecomAbstractApi(models.AbstractModel):
_name = "wecom.abstract_api"
_description = "Wecom Abstract API"
code = fields.Integer("API error code", copy=False)
message = fields.Char("API error message", copy=False)
def get_api_debug(self):
"""
获取 API 调试模式
"""
ir_config = self.env["ir.config_parameter"].sudo()
return True if ir_config.get_param("wecom.debug_enabled") == "True" else False
def getAccessToken(self):
raise NotImplementedError
def refreshAccessToken(self):
raise NotImplementedError
def getSuiteAccessToken(self):
raise NotImplementedError
def refreshSuiteAccessToken(self):
raise NotImplementedError
def getProviderAccessToken(self):
raise NotImplementedError
def refreshProviderAccessToken(self):
raise NotImplementedError
def httpCall(self, urlType, args=None, include_agentid=False):
"""
调用API
:param urlType : 服务端API类型和请求方式("GET" or "POST")
:param args : 请求参数
:param include_agentid : 标识args含有 "agentid" 关键字,需要进行处理
:returns 返回结果
"""
shortUrl = urlType[0]
method = urlType[1]
response = {}
for retryCnt in range(0, 3):
try:
if "POST" == method:
url = self.__makeUrl(shortUrl)
if "agentid" in args and include_agentid: # type: ignore
url = self.__appendArgs(url, {"agentid": args["agentid"]}) # type: ignore
del args["agentid"] # type: ignore
response = self.__httpPost(url, args)
elif "GET" == method:
url = self.__makeUrl(shortUrl)
url = self.__appendArgs(url, args)
response = self.__httpGet(url)
else:
raise ApiException(-1, _("unknown method type"))
except Exception as e:
raise ApiException(-2, e) # 其他错误
# 检查令牌是否过期
if self.__tokenExpired(response.get("errcode")):
self.__refreshToken(shortUrl)
retryCnt += 1
continue
else:
break
# 检测响应
return self.__checkResponse(response)
def httpPostFile(self, urlType, args=None, data=None, headers=None):
shortUrl = urlType[0]
response = {}
for retryCnt in range(0, 3):
url = self.__makeUrl(shortUrl)
url = self.__appendArgs(url, args)
response = self.__httpPostFile(url, data, headers)
# 检查令牌是否过期
if self.__tokenExpired(response.get("errcode")):
self.__refreshToken(shortUrl)
retryCnt += 1
continue
else:
break
return self.__checkResponse(response)
@staticmethod
def __appendArgs(url, args):
if args is None:
return url
for key, value in args.items():
if "?" in url:
url += "&" + key + "=" + value
else:
url += "?" + key + "=" + value
return url
@staticmethod
def __makeUrl(shortUrl):
base = "https://qyapi.weixin.qq.com"
if shortUrl[0] == "/":
return base + shortUrl
else:
return base + "/" + shortUrl
def __appendToken(self, url):
if "SUITE_ACCESS_TOKEN" in url:
return url.replace("SUITE_ACCESS_TOKEN", self.getSuiteAccessToken())
elif "PROVIDER_ACCESS_TOKEN" in url:
return url.replace("PROVIDER_ACCESS_TOKEN", self.getProviderAccessToken())
elif "ACCESS_TOKEN" in url:
return url.replace("ACCESS_TOKEN", self.getAccessToken())
else:
return url
def __httpPost(self, url, args):
realUrl = self.__appendToken(url)
if self.get_api_debug() is True:
print("Wecom API POST", realUrl, args)
return requests.post(
realUrl, data=json.dumps(args, ensure_ascii=False).encode("utf-8")
).json()
def __httpPostFile(self, url, data, headers):
realUrl = self.__appendToken(url)
if self.get_api_debug() is True:
print("Wecom API POST FILE", realUrl, data, headers)
# response = requests.post(url=realUrl, data=data, headers=headers).json()
return requests.post(url=realUrl, data=data, headers=headers).json()
def __post_file(self, url, media_file):
if self.get_api_debug() is True:
print("Wecom API POST FILE", url, media_file)
return requests.post(url, file=media_file).json() # type: ignore
def __httpGet(self, url):
realUrl = self.__appendToken(url)
if self.get_api_debug() is True:
print("Wecom API GET", realUrl)
return requests.get(realUrl).json()
@staticmethod
def __checkResponse(response):
"""
检查 返回 值是否合法,
"""
errCode = response.get("errcode")
errMsg = response.get("errmsg")
if errCode == 0:
return response
else:
raise ApiException(errCode, errMsg)
@staticmethod
def __tokenExpired(errCode):
"""
检查 令牌 是否过期
"""
if errCode == 40014 or errCode == 42001 or errCode == 42007 or errCode == 42009:
return True
else:
return False
def __refreshToken(self, url):
"""
刷新令牌
"""
if "SUITE_ACCESS_TOKEN" in url:
self.refreshSuiteAccessToken()
elif "PROVIDER_ACCESS_TOKEN" in url:
self.refreshProviderAccessToken()
elif "ACCESS_TOKEN" in url:
self.refreshAccessToken()
-96
View File
@@ -1,96 +0,0 @@
# -*- coding: utf-8 -*-
import json
import requests
from datetime import datetime, timedelta
import time
from odoo import api, fields, models, SUPERUSER_ID, _
from collections import OrderedDict
class WecomClientApi(models.TransientModel):
_name = "wecom.client_api"
_description = "Wecom Client API"
@api.model
def geContactsDisplayAgentConfig(self, postDatas):
"""
获取企业微信通讯录展示组件的应用配置
"""
configs = []
result = {}
for data in postDatas:
config = {}
company = self.env["res.company"].search([("id", "=", data["company_id"])])
self_built_app = company.self_built_app_id
timestamp = int(time.time())
nonceStr = self.env["wecomapi.tools.security"].random_str(8)
jsapi_ticket = company.contacts_app_id.jsapi_ticket
signature = self.env["wecomapi.tools.security"].generate_jsapi_signature(
jsapi_ticket, nonceStr, timestamp
)
config.update(
{
"company_id": company.id,
"corpid": company.corpid,
"agentid": self_built_app.agentid,
"timestamp": timestamp,
"nonceStr": nonceStr,
"signature": signature,
"jsApiList": ["selectExternalContact"],
}
)
configs.append(config)
result.update(
{
"code": 0,
"msg": "ok",
"configs": configs,
}
)
return result
@api.model
def getBarcodeWXConfig(self, postDatas):
"""
获取企业微信扫码组件的应用配置
"""
configs = []
result = {}
for data in postDatas:
config = {}
company = (
self.env["res.company"].sudo().search([("id", "=", data["company_id"])])
)
# wecom_apps = self.env['wecom.apps'].sudo().search([('type','=','self'), ('subtype_ids.code','=','auth')], limit=1)
timestamp = int(time.time())
nonceStr = self.env["wecomapi.tools.security"].sudo().random_str(8)
jsapi_ticket = company.corp_jsapi_ticket
signature = (
self.env["wecomapi.tools.security"]
.sudo()
.generate_jsapi_signature(jsapi_ticket, nonceStr, timestamp)
)
config.update(
{
"company_id": company.id,
"corpid": company.corpid,
# "agentid":wecom_apps.agentid,
"timestamp": timestamp,
"nonceStr": nonceStr,
"signature": signature,
"jsApiList": ["scanQRCode"],
}
)
configs.append(config)
result.update(
{
"code": 0,
"msg": "ok",
"configs": configs,
}
)
return result
-260
View File
@@ -1,260 +0,0 @@
# -*- coding: utf-8 -*-
import logging
from odoo import api, models, _
from odoo.addons.wecom_api.api.wecom_abstract_api import ApiException # type: ignore
from datetime import datetime, timedelta
import pytz
import json
_logger = logging.getLogger(__name__)
class WeComMessageApi(models.AbstractModel):
_name = "wecom.message.api"
_description = "WeCom Message API"
def get_message_api(self, company):
""""""
wxapi = self.env["wecom.service_api"].InitServiceApi(
company, "message_secret", "message"
)
return wxapi
def send_by_api(self, message, api):
params = self.env["ir.config_parameter"].sudo()
debug = params.get_param("wecom.debug_enabled")
try:
del message["company"] # 删除message中的 company
response = api.httpCall(
self.env["wecom.service_api_list"].get_server_api_call("MESSAGE_SEND"),
message,
)
return response
except ApiException as ex:
if debug:
_logger.warning(_("Error sending message: %s") % (ex))
def build_message(
self,
msgtype,
toall=None,
touser=None,
toparty=None,
totag=None,
subject=None,
media_id=None,
description=None,
author_id=None,
body_html=None,
body_json=None,
body_markdown=None,
safe=None,
enable_id_trans=None,
enable_duplicate_check=None,
duplicate_check_interval=None,
company=None,
):
"""
构建消息
:msgtype: 消息类型,根据消息类型来构建消息内容
:toall: 发送给全体人员
:touser: 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。
特殊情况:指定为”@all”,则向该企业应用的全部成员发送
:toparty: 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
当touser为”@all”时忽略本参数
:totag: 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
当touser为”@all”时忽略本参数
:subject: 标题 ,不同消息类型,长度限制不一样
:media_id: media_id, 可以通过素材管理接口获得
:body_html: 图文消息的内容,支持html标签,不超过666 K个字节,仅用于图文消息(mpnews)
:body_json: 非图文消息的内容,不同消息类型,长度限制不一样
:safe: 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
:enable_id_trans: 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
:enable_duplicate_check: Indicates whether to enable duplicate message checking. 0 indicates no, 1 indicates yes. The default is 0
:duplicate_check_interval: Indicates whether the message check is repeated. The default is 1800s and the maximum is no more than 4 hours
"""
if not company:
company = self.env.company
messages_content = self.get_messages_content(
msgtype,
description,
author_id,
body_html,
body_json,
body_markdown,
subject,
media_id,
)
messages_options = self.get_messages_options(
msgtype,
safe,
enable_id_trans,
enable_duplicate_check,
duplicate_check_interval,
)
messages = {
"touser": "@all" if toall else touser,
"toparty": "" if toall else toparty,
"totag": "" if toall else totag,
"msgtype": msgtype,
"agentid": company.message_agentid,
"company": company,
}
messages.update(messages_content)
messages.update(messages_options)
return messages
@api.model
def send_message(self, message):
"""
发送一条企业微信消息 到多个人员
"""
return self.send_by_api(message)
@api.model
def _send_message_batch(self, messages):
"""
批量模式发送企业微信消息
"""
params = {
"messages": messages,
}
return self._wecom_message_send_api(params)
def get_messages_content(
self,
msgtype,
description=None,
author_id=None,
body_html=None,
body_json=None,
body_markdown=None,
subject=None,
media_id=None,
):
# material_info = (
# self.env["wecom.material"].sudo().browse(int(media_id)).read(["name"])
# )
messages_content = {}
if msgtype == "text":
# 文本消息
messages_content = {
"text": body_json,
}
elif msgtype == "mpnews":
# 图文消息(mpnews)
material = (
self.sudo()
.env["wecom.material"]
.search(
[
("id", "=", media_id),
],
limit=1,
)
)
# material_media_id = self.check_material_file_expiration(material)
messages_content = {
"mpnews": {
"articles": [
{
"title": subject,
"thumb_media_id": material._check_material_file_expiration(),
"author": author_id.display_name,
"content": body_html,
"digest": description,
}
]
},
}
elif msgtype == "markdown":
# markdown消息
messages_content = {
"markdown": {
"content": body_markdown,
}
}
elif msgtype == "template_card":
# 模板卡片消息
messages_content = {
"template_card": json.loads(body_json, strict=False),
}
return messages_content
def get_messages_options(
self,
msgtype,
safe=None,
enable_id_trans=None,
enable_duplicate_check=None,
duplicate_check_interval=None,
):
"""[summary]
获取企业微信消息的选项
Args:
msgtype ([type]): [description]
safe ([type], optional): [description]. Defaults to None.
enable_id_trans ([type], optional): [description]. Defaults to None.
enable_duplicate_check ([type], optional): [description]. Defaults to None.
duplicate_check_interval ([type], optional): [description]. Defaults to None.
Returns:
[type]: [description]
"""
messages_options = {
"safe": int(safe),
"enable_id_trans": int(enable_id_trans),
"enable_duplicate_check": int(enable_duplicate_check),
"duplicate_check_interval": duplicate_check_interval,
}
if msgtype == "markdown":
# markdown消息
del messages_options["safe"]
del messages_options["enable_id_trans"]
if msgtype == "template_card":
# 模板卡片消息
del messages_options["safe"]
return messages_options
# def check_material_file_expiration(self, material):
# """[summary]
# 检查素材文件是否过期
# Args:
# material ([type]): [description]
# Returns:
# [type]: [description]
# """
# media_id = ""
# if material.created_at:
# # 有创建日期
# created_time = material.created_at
# MAX_FAIL_TIME = 3
# # 检查是否超过3天
# overdue = self.env["wecom.tools"].cheeck_days_overdue(
# created_time, MAX_FAIL_TIME
# )
# if overdue:
# # 临时素材超期,重新上传
# material.upload_media()
# media_id = material.media_id
# else:
# # 无创建日期,执行第一次上传临时素材
# material.upload_media()
# media_id = material.media_id
# return media_id
-301
View File
@@ -1,301 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
import logging
import base64
import random
import hashlib
import time
import struct
from Crypto.Cipher import AES
import xml.etree.cElementTree as ET
import socket
import hashlib
import werkzeug.urls
import werkzeug.utils
_logger = logging.getLogger(__name__)
#########################################################################
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseXml_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateCorpid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnXml_Error = -40011
class FormatException(Exception):
pass
def throw_exception(message, exception_class=FormatException):
"""自定义引发异常函数"""
raise exception_class(message)
class SHA1:
"""计算企业微信的消息签名接口"""
def getSHA1(self, token, timestamp, nonce, encrypt):
"""用SHA1算法生成安全签名
@param token: 票据
@param timestamp: 时间戳
@param encrypt: 密文
@param nonce: 随机字符串
@return: 安全签名
"""
try:
sortlist = [token, timestamp, nonce, encrypt]
sortlist.sort()
sha = hashlib.sha1()
sha.update("".join(sortlist).encode())
return WXBizMsgCrypt_OK, sha.hexdigest()
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return WXBizMsgCrypt_ComputeSignature_Error, None
class XMLParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# xml消息模板
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
<TimeStamp>%(timestamp)s</TimeStamp>
<Nonce><![CDATA[%(nonce)s]]></Nonce>
</xml>"""
def extract(self, xmltext):
"""提取出xml数据包中的加密消息
@param xmltext: 待提取的xml字符串
@return: 提取出的加密消息字符串
"""
try:
xml_tree = ET.fromstring(xmltext)
encrypt = xml_tree.find("Encrypt")
return WXBizMsgCrypt_OK, encrypt.text # type: ignore
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return WXBizMsgCrypt_ParseXml_Error, None, None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成xml消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的xml字符串
"""
resp_dict = {
"msg_encrypt": encrypt,
"msg_signaturet": signature,
"timestamp": timestamp,
"nonce": nonce,
}
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_xml
class PKCS7Encoder:
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = chr(amount_to_pad)
return text + (pad * amount_to_pad).encode()
def decode(self, decrypted):
"""删除解密后明文的补位字符
@param decrypted: 解密后的明文
@return: 删除补位字符后的明文
"""
pad = ord(decrypted[-1])
if pad < 1 or pad > 32:
pad = 0
return decrypted[:-pad]
class Prpcrypt(object):
"""提供接收和推送给企业微信消息的加解密接口"""
def __init__(self, key):
# self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
self.mode = AES.MODE_CBC
def encrypt(self, text, receiveid):
"""对明文进行加密
@param text: 需要加密的明文
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = text.encode()
text = (
self.get_random_str()
+ struct.pack("I", socket.htonl(len(text)))
+ text
+ receiveid.encode()
)
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
text = pkcs7.encode(text)
# 加密
cryptor = AES.new(self.key, self.mode, self.key[:16]) # type: ignore
try:
ciphertext = cryptor.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return WXBizMsgCrypt_EncryptAES_Error, None
def decrypt(self, text, receiveid):
"""对解密后的明文进行补位删除
@param text: 密文
@return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key, self.mode, self.key[:16]) # type: ignore
# 使用BASE64对密文进行解码,然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return WXBizMsgCrypt_DecryptAES_Error, None
try:
pad = plain_text[-1]
# 去掉补位字符串
# pkcs7 = PKCS7Encoder()
# plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
xml_len = socket.ntohl(struct.unpack("I", content[:4])[0])
xml_content = content[4 : xml_len + 4]
from_receiveid = content[xml_len + 4 :]
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return WXBizMsgCrypt_IllegalBuffer, None
if from_receiveid.decode("utf8") != receiveid:
return WXBizMsgCrypt_ValidateCorpid_Error, None
return 0, xml_content
def get_random_str(self):
""" 随机生成16位字符串
@return: 16位字符串
"""
return str(random.randint(1000000000000000, 9999999999999999)).encode()
class WecomMsgCrypt(object):
# 消息加解密
# _name = "wecom.msg_crypt"
# _description = "Wecom Message encryption and decryption"
def __init__(self, sToken, sEncodingAESKey, sReceiveId):
"""
初始化消息加解密对象
:return:
"""
try:
self.key = base64.b64decode(sEncodingAESKey + "=")
assert len(self.key) == 32
except:
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
# return WXBizMsgCrypt_IllegalAesKey,None
self.m_sToken = sToken
self.m_sReceiveId = sReceiveId
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
"""验证URL
@param sMsgSignature: 签名串,对应URL参数的msg_signature
@param sTimeStamp: 时间戳,对应URL参数的timestamp
@param sNonce: 随机串,对应URL参数的nonce
@param sEchoStr: 随机串,对应URL参数的echostr
@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
@return: 成功0,失败返回对应的错误码
"""
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId)
return ret, sReplyEchoStr
def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
"""将企业回复用户的消息加密打包
@param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
return:成功0,sEncryptMsg,失败返回对应的错误码None
"""
pc = Prpcrypt(self.key)
ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
encrypt = encrypt.decode("utf8") # type: ignore
if ret != 0:
return ret, None
if timestamp is None:
timestamp = str(int(time.time()))
# 生成安全签名
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
if ret != 0:
return ret, None
xmlParse = XMLParse()
return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce)
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
"""检验消息的真实性,并且获取解密后的明文
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
# @param sNonce: 随机串,对应URL参数的nonce
# @param sPostData: 密文,对应POST请求的数据
# xml_content: 解密后的原文,当return返回0时有效
# @return: 成功0,失败返回对应的错误码
# 验证安全签名
"""
xmlParse = XMLParse()
ret, encrypt = xmlParse.extract(sPostData) # type: ignore
if ret != 0:
return ret, None
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId)
return ret, xml_content
-100
View File
@@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
import json
import requests
from datetime import datetime, timedelta
from odoo import api, fields, models, SUPERUSER_ID, _
# from .wecom_abstract_api import ApiException
class WecomServerApi(models.TransientModel):
_inherit = "wecom.abstract_api"
_name = "wecom.service_api"
_description = "Wecom Server API"
_table = "wecom_service_api"
corpid = fields.Char(string="Corp Id", readonly=True)
secret = fields.Char(string="Secret", readonly=True)
access_token = fields.Char(string="Access Token", readonly=True, default=None)
token_expiration_time = fields.Datetime(
string="Token Expiration Time", readonly=True
)
@api.model
def InitServiceApi(self, corpid, secret):
"""
初始化企业微信API 对象
:param corpid : 企业ID
:param secret : 应用密钥
:returns 模型"wecom.service_api"对象
"""
api = self.search(
[
("corpid", "=", corpid),
("secret", "=", secret),
],
limit=1,
)
if not api:
# 创建API令牌记录
api = self.sudo().create(
{
"corpid": corpid,
"secret": secret,
}
)
if api["access_token"] is False or api["access_token"] == "": # type: ignore
# token为空,刷新API令牌记录
api.refreshAccessToken() # type: ignore
if (
api["token_expiration_time"] is False # type: ignore
or api["token_expiration_time"] < datetime.now() # type: ignore
):
# token过期,刷新API令牌记录
api.refreshAccessToken() # type: ignore
return api
def getAccessToken(self):
"""
获取令牌
"""
if self.access_token is None:
self.refreshAccessToken()
return self.access_token
def refreshAccessToken(self):
"""
刷新模型 'wecom.service_api' 的令牌
同时刷新发起请求的模型 'wecom.apps' 的令牌
"""
response = self.httpCall( # type: ignore
self.env["wecom.service_api_list"].get_server_api_call("GET_ACCESS_TOKEN"),
{"corpid": self.corpid, "corpsecret": self.secret},
)
expiration_second = timedelta(seconds=response["expires_in"])
dict = {
"access_token": response.get("access_token"),
"token_expiration_time": datetime.now() + expiration_second,
}
self.sudo().write(dict)
# 写对应的应用的值
compay_id = self.env["res.company"].search(
[("corpid", "=", self.corpid)], limit=1
)
app_id = self.env["wecom.apps"].search(
[("company_id", "=", compay_id.id), ("secret", "=", self.secret)], limit=1
)
app_id.sudo().write(dict)
def get_api_debug(self):
"""
获取 API 调试模式
"""
ir_config = self.env["ir.config_parameter"].sudo()
return True if ir_config.get_param("wecom.debug_enabled") == "True" else False
-3
View File
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import main
-131
View File
@@ -1,131 +0,0 @@
# -*- coding: utf-8 -*-
import json
import logging
import xml.etree.cElementTree as ET
from lxml import etree
import sys
from odoo.addons.wecom_api.api.wecom_msg_crtpt import WecomMsgCrypt # type: ignore
from odoo import http, models, fields, _
from odoo.http import request
from odoo.http import Response
_logger = logging.getLogger(__name__)
class StripeController(http.Controller):
""" """
@http.route(
["/wecom_callback", "/wecom_callback/"],
type="http",
auth="public",
methods=["GET", "POST"],
)
def WecomCallbackService(self): # type: ignore
"""
企业微信回调服务
"""
@http.route(
["/wecom_callback/<int:id>/<string:service>", "/wecom_callback/contacts"],
type="http",
auth="public",
methods=["GET", "POST"],
csrf=False,
)
def WecomCallbackService(self, service, id, **kw):
"""
企业微信回调服务
:param id: 公司id
:param service: 回调服务名称 code
文档URL: https://work.weixin.qq.com/api/doc/90000/90135/90930#3.2%20%E6%94%AF%E6%8C%81Http%20Post%E8%AF%B7%E6%B1%82%E6%8E%A5%E6%94%B6%E4%B8%9A%E5%8A%A1%E6%95%B0%E6%8D%AE
"""
company_id = request.env["res.company"].sudo().search([("id", "=", id)]) # type: ignore
sCorpID = company_id.corpid
callback_service = (
company_id.contacts_app_id.app_callback_service_ids.sudo().search(
[
("app_id", "=", company_id.contacts_app_id.id),
("code", "=", service),
"|",
("active", "=", True),
("active", "=", False),
]
)
)
if callback_service.active is False:
_logger.info(
_("App [%s] does not have service [%s] enabled")
% (company_id.contacts_app_id.name, callback_service.name)
)
return Response("success", status=200)
else:
wxcpt = WecomMsgCrypt(
callback_service.callback_url_token,
callback_service.callback_aeskey,
sCorpID,
)
# 获取企业微信发送的相关参数
sVerifyMsgSig = kw["msg_signature"]
sVerifyTimeStamp = kw["timestamp"]
sVerifyNonce = kw["nonce"]
if request.httprequest.method == "GET": # type: ignore
# ^ 假设企业的接收消息的URL设置为http://api.3dept.com。
# ^ 企业管理员在保存回调配置信息时,企业微信会发送一条验证消息到填写的URL,请求内容如下:
# ^ 请求方式:GET
# ^ http://api.3dept.com/?msg_signature=ASDFQWEXZCVAQFASDFASDFSS&timestamp=13500001234&nonce=123412323&echostr=ENCRYPT_STR
# ^ 请求参数:
# ^ msg_signature: 企业微信加密签名,msg_signature计算结合了企业填写的token、请求中的timestamp、nonce、加密的消息体。
# ^ timestamp: 时间戳。与nonce结合使用,用于防止请求重放攻击。
# ^ nonce: 随机数。用于保证签名不可预测。与timestamp结合使用,用于防止请求重放攻击。
# ^ echostr: 加密的随机字符串。用于保证签名不可预测。
sVerifyEchoStr = kw["echostr"]
ret, msg = wxcpt.VerifyURL(
sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr
)
if ret != 0:
logging.error("ERR: VerifyURL ret: " + str(ret))
sys.exit(1)
return msg
if request.httprequest.method == "POST": # type: ignore
# ^ 假设企业的接收消息的URL设置为http://api.3dept.com。
# ^ 当用户触发回调行为时,企业微信会发送回调消息到填写的URL,请求内容如下:
# ^ 请求方式:POST
# ^ 请求地址 :http://api.3dept.com/?msg_signature=ASDFQWEXZCVAQFASDFASDFSS&timestamp=13500001234&nonce=123412323
# ^ 请求参数:
# ^ msg_signature: 企业微信加密签名,msg_signature计算结合了企业填写的token、请求中的timestamp、nonce、加密的消息体。
# ^ timestamp: 时间戳。与nonce结合使用,用于防止请求重放攻击。
# ^ nonce: 随机数。用于保证签名不可预测。与timestamp结合使用,用于防止请求重放攻击。
# ^ ToUserName: 企业微信的CorpID,当为第三方应用回调事件时,CorpID的内容为suiteid
# ^ AgentID: 接收的应用id,可在应用的设置页面获取。仅应用相关的回调会带该字段。
# ^ Encrypt: 消息结构体加密后的字符串
sReqData = request.httprequest.data # type: ignore
ret, msg = wxcpt.DecryptMsg(
sReqData, sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce
)
if ret != 0:
logging.error("ERR: DecryptMsg ret: " + str(ret))
sys.exit(1)
# 解密成功,msg即明文的xml消息结构体
# xml_tree = etree.fromstring(msg) # xml解析为一个xml元素
# print("解密成功", msg)
try:
return (
request.env["wecom.app.event_type"] # type: ignore
.sudo()
.with_context(xml_tree=msg, company_id=company_id)
.handle_event()
) # 传递xml元素和公司
except:
pass
finally:
# ^ 正确响应企业微信本次的POST请求,企业微信将不会再次发送请求
# ^ ·企业微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次
# ^ ·当接收成功后,http头部返回200表示接收ok,其他错误码企业微信后台会一律当做失败并发起重试
# return Response("success", status=200)
pass
-24
View File
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
</data>
<data noupdate="1">
<record model="ir.config_parameter" id="wecom_global_error_code_url">
<field name="key">wecom.global_error_code_url</field>
<field name="value">https://developer.work.weixin.qq.com/document/path/90313</field>
</record>
<!-- !2022-10-03更新 -->
<!-- ! 用于定位 错误代码 的路径表达式-->
<record model="ir.config_parameter" id="global_error_code_item_selection_code">
<field name="key">wecom.global_error_code_item_selection_code</field>
<field name="value">//h5</field>
</record>
</data>
</odoo>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
</data>
</odoo>
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ----------------------------------------
# 使用爬虫爬取微信错误码 生成 XML 数据文件
# ----------------------------------------
import os
from io import BytesIO
import requests
from lxml import etree
nodes = "//table/tbody/tr"
url = "https://developer.work.weixin.qq.com/document/path/90313"
page_text = requests.get(url=url).text
tree = etree.HTML(page_text) # type: ignore
lines = tree.xpath(nodes)
# 输出一个错误代码列表
codes = []
for line in lines:
code = line.xpath("./td[1]/text()")[0]
try:
name = line.xpath("./td[2]/text()")[0]
except:
name = line.xpath("./td[3]/text()")[0]
# print(name)
# xml 转义
if '"' in name:
name.replace('"', "&quot;")
if "&" in name:
name.replace('"', "&amp;")
if "<" in name:
name.replace('"', "&lt;")
if "<" in name:
name.replace(">", "&gt;")
codes.append(
{
"code": int(code),
"name": name,
}
)
# 排序
new_codes = sorted(codes, key=lambda c: int(c["code"]))
# ----------------------------
# https://lxml.de/tutorial.html
# ----------------------------
# 生成生成根节点
root = etree.Element("odoo") # type: ignore
# 生成 data 节点
data_node = etree.Element("data", attrib={"noupdate": "0"}) # type: ignore
root.append(data_node)
for code in new_codes:
id = "wechat_error_codes_"
if code == -1:
id += "minus_1"
else:
id += str(code["code"])
record_attrs = {
"id": id,
"model": "wechat.error_codes",
}
record_node = etree.Element("record", attrib=record_attrs) # type: ignore
code_field_node = etree.Element("field", attrib={"name": "code"}) # type: ignore
code_field_node.text = str(code["code"])
name_field_node = etree.Element("field", attrib={"name": "name"}) # type: ignore
name_field_node.text = code["name"]
record_node.append(code_field_node)
record_node.append(name_field_node)
data_node.append(record_node)
# 保存xml
xml = (
"""<?xml version="1.0" encoding="utf-8"?>
%s
"""
% etree.tostring(root, pretty_print=True).decode()
)
current_dir = os.path.dirname(os.path.abspath(__file__))
xml_file_name = "reptile.xml"
xml_file_path = os.path.join(current_dir, xml_file_name)
if not os.path.exists(xml_file_path):
os.system(r"touch {}".format(xml_file_path)) # 调用系统命令行来创建文件
with open(xml_file_path, "w", encoding="gb2312") as f:
f.write(xml)
File diff suppressed because it is too large Load Diff
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<record id="wecom_server_api_error_python_code" model="wecom.service_api_error">
<field name="name">Python Code Error</field>
<field name="code">-2</field>
<field name="method">Python Code Error,Please check your code for errors. Python代码错误,请检查您的代码是否有错误</field>
<field name="sequence">0</field>
</record>
</data>
</odoo>
-371
View File
@@ -1,371 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * wecom_api
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0beta\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-10-04 07:34+0000\n"
"PO-Revision-Date: 2022-10-04 15:35+0800\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: \n"
"X-Generator: Poedit 3.1.1\n"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_abstract_api__code
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__code
msgid "API error code"
msgstr "API错误代码"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_abstract_api__message
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__message
msgid "API error message"
msgstr "API错误消息"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__access_token
msgid "Access Token"
msgstr "访问令牌"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__request_type
msgid "Api Request Type"
msgstr "Api请求类型"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__type
msgid "Api Type"
msgstr "Api类型"
#. module: wecom_api
#: code:addons/wecom_api/controllers/main.py:0
#, python-format
msgid "App [%s] does not have service [%s] enabled"
msgstr "应用 [%s] 未启用服务 [%s]"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__6
msgid "Application management"
msgstr "应用管理"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__1
msgid "Base"
msgstr "基础"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__2
msgid "Contacts"
msgstr "通讯录管理"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__13
msgid "Corp Chain"
msgstr "上下游(公司链)"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__12
msgid "Corp Group"
msgstr "企业互联"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__corpid
msgid "Corp Id"
msgstr "企业ID"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__create_uid
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__create_uid
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__create_uid
msgid "Created by"
msgstr "创建者"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__create_date
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__create_date
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__create_date
msgid "Created on"
msgstr "创建于"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__3
msgid "Customer contact"
msgstr "客户联系"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__description
msgid "Description"
msgstr "说明"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__display_name
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__display_name
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__display_name
msgid "Display Name"
msgstr "显示名称"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__10
msgid "Efficiency tools"
msgstr "效率工具"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__15
msgid "Electronic invoice"
msgstr "电子发票"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__code
msgid "Error code"
msgstr "错误代码"
#. module: wecom_api
#: code:addons/wecom_api/tools/action.py:0
#, python-format
msgid ""
"Error code: %s,Error description: %s,Error troubleshooting method: %s,Error "
"Details:%s"
msgstr "错误代码:%s,错误描述:%s,故障排除方法:%s,出错详细信息:%s"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__name
msgid "Error description"
msgstr "错误说明"
#. module: wecom_api
#: code:addons/wecom_api/api/wecom_message_api.py:0
#, python-format
msgid "Error sending message: %s"
msgstr "发送消息时出错:%s"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__token_expiration_time
msgid "Token Expiration Time"
msgstr "Token过期时间"
#. module: wecom_api
#: code:addons/wecom_api/tools/action.py:0
#, python-format
msgid "Failed"
msgstr "失败"
#. module: wecom_api
#: code:addons/wecom_api/models/wecom_server_api_error.py:0
#, python-format
msgid "Failed to pull WeCom global error code, reason:%s"
msgstr "无法拉取企微全局错误代码,原因:%s"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__request_type__get
msgid "GET"
msgstr ""
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__id
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__id
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__id
msgid "ID"
msgstr ""
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__5
msgid "Identity authentication"
msgstr "身份验证"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api____last_update
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error____last_update
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list____last_update
msgid "Last Modified on"
msgstr "最后修改"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__write_uid
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__write_uid
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__write_uid
msgid "Last Updated by"
msgstr "最后更新者"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__write_date
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__write_date
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__write_date
msgid "Last Updated on"
msgstr "最后更新于"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__8
msgid "Media material"
msgstr "素材管理"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__7
msgid "Message push"
msgstr "消息推送"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__9
msgid "OA"
msgstr ""
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__request_type__post
msgid "POST"
msgstr ""
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__18
msgid "Report"
msgstr "政民沟通"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__function_name
msgid "Request Function Name"
msgstr "请求函数名称"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__name
msgid "Request Name"
msgstr "请求名称"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__short_url
msgid "Request Short Url"
msgstr "请求短Url"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__16
msgid "School"
msgstr "家校沟通"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__17
msgid "School Apps"
msgstr "家校应用"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api__secret
msgid "Secret"
msgstr "密钥"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__sequence
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_list__sequence
msgid "Sequence"
msgstr "序列"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__14
msgid "Session content archiving"
msgstr "会话内容存档"
#. module: wecom_api
#: code:addons/wecom_api/models/wecom_server_api_error.py:0
#, python-format
msgid "Start pulling the global error code of WeCom."
msgstr "开始拉取企微全局错误代码。"
#. module: wecom_api
#: code:addons/wecom_api/models/wecom_server_api_error.py:0
#, python-format
msgid "Successfully pulled the WeCom global error code!"
msgstr "已成功拉取企微全局错误代码!"
#. module: wecom_api
#: model:ir.model.constraint,message:wecom_api.constraint_wecom_service_api_list_function_name_uniq
msgid "The function is unique !"
msgstr "功能是唯一的!"
#. module: wecom_api
#: model:ir.model.fields,field_description:wecom_api.field_wecom_service_api_error__method
msgid "Treatment method"
msgstr "排查方法"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecom_message_api
msgid "WeCom Message API"
msgstr "企微消息API"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__4
msgid "Wechat customer service"
msgstr "微信客服"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecomapi_tools_action
msgid "Wecom API Tools - Action"
msgstr "企微API工具-动作"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecomapi_tools_data
msgid "Wecom API Tools - Data"
msgstr "企微API工具-数据"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecomapi_tools_datetime
msgid "Wecom API Tools - Datetime"
msgstr "企微API工具-日期时间"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecomapi_tools_file
msgid "Wecom API Tools - File"
msgstr "企微API工具-文件"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecomapi_tools_message
msgid "Wecom API Tools - Message"
msgstr "企微API工具-消息"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecomapi_tools_security
msgid "Wecom API Tools - Security"
msgstr "企微API工具-安全"
#. module: wecom_api
#: code:addons/wecom_api/tools/action.py:0
#, python-format
msgid "Wecom API error: %s, error name: %s, error message: %s"
msgstr "企微API错误: %s, 错误名称: %s, 错误消息: %s"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecom_abstract_api
msgid "Wecom Abstract API"
msgstr "企微抽象API"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecom_service_api
msgid "Wecom Server API"
msgstr "企微服务端API"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecom_service_api_error
msgid "Wecom Server API Error"
msgstr "企微服务端API错误"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecom_service_api_list
msgid "Wecom Server API List"
msgstr "企微服务端API列表"
#. module: wecom_api
#: model:ir.model,name:wecom_api.model_wecom_tools
msgid "Wecom Tools"
msgstr "企微工具"
#. module: wecom_api
#: model:ir.model.fields.selection,name:wecom_api.selection__wecom_service_api_list__type__11
msgid "externalpay"
msgstr "企业支付"
#. module: wecom_api
#: code:addons/wecom_api/api/wecom_abstract_api.py:0
#, python-format
msgid "unknown method type"
msgstr "未知方法类型"
-5
View File
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
from . import wecom_server_api_error
from . import wecom_server_api_list
-8
View File
@@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
-64
View File
@@ -1,64 +0,0 @@
# -*- coding: utf-8 -*-
import requests
import pandas as pd
pd.set_option('display.max_columns', 10)
pd.set_option("max_colwidth", 200)
pd.set_option("display.width", 200)
pd.set_option('display.max_rows', 200)
from lxml import etree
url = "https://developer.work.weixin.qq.com/document/path/90313"
anchor = "//h2[@data-sign='12309549435022fd54b0549f5968b4c2']"
codes = "//h5"
methods ="//p[@data-type='p']"
page_text = requests.get(url=url).text
tree = etree.HTML(page_text) # type: ignore
anchor_elemen = tree.xpath(anchor)
codes_elements = tree.xpath(codes)
methods_elements = tree.xpath(methods)
methods = []
for code_element in codes_elements:
code_element_str = code_element.xpath("text()")[0]
error_code = code_element_str.split(":", 1)[1:][0]
method_element = code_element.getnext()
method = etree.tostring(method_element, encoding="utf-8", pretty_print=True ).decode() # type: ignore
if " " in error_code:
# 一个元素存在多个错误码
multiple_codes = error_code.split(" ", 1)
for multiple_code in multiple_codes:
multiple_dic = {}
multiple_dic["code"] = multiple_code
multiple_dic["method"] = method
methods.append(multiple_dic)
else:
dic = {}
dic["code"] = error_code
dic["method"] = method
methods.append(dic)
table = tree.xpath("//div[@class='cherry-table-container']/table") # 取出表格
table = etree.tostring(
table[0], encoding="utf-8" # type: ignore
).decode() # 将第一个表格转成string格式
table = table.replace("<th>错误码</th>", "<th>code</th>")
table = table.replace("<th>错误说明</th>", "<th>name</th>")
table = table.replace("<th>排查方法</th>", "<th>method</th>")
df = pd.read_html(table, encoding="utf-8", header=0)[0] # pandas读取table
if 'Unnamed: 3' in df.columns:
del df['Unnamed: 3']
error_results = list(df.T.to_dict().values()) # 转换成列表嵌套字典的格式
errors = []
for index, error in enumerate(error_results):
error["sequence"] = index
errors.append(error)
df = pd.DataFrame(errors)
print(df)
-175
View File
@@ -1,175 +0,0 @@
# -*- coding: utf-8 -*-
import operator
import requests
import logging
from urllib.parse import quote, unquote
import pandas as pd
# pd.set_option("max_colwidth", 4096)
# pd.set_option("max_colwidth", 10)
from lxml import etree
from odoo import api, fields, models, SUPERUSER_ID, _
_logger = logging.getLogger(__name__)
class WecomServerApiError(models.Model):
_name = "wecom.service_api_error"
_description = "Wecom Server API Error"
_order = "sequence"
name = fields.Char(
"Error description",
required=True,
readonly=True,
)
code = fields.Integer(
"Error code",
required=True,
readonly=True,
)
method = fields.Html(
"Treatment method",
readonly=True,
)
sequence = fields.Integer(default=0)
def get_error_by_code(self, code):
res = self.search(
[("code", "=", code)],
limit=1,
)
return {
"code": res.code, # type: ignore
"name": res.name, # type: ignore
"method": res.method, # type: ignore
}
def cron_pull_global_error_code(self):
self.get_api_error_eode()
@api.model
def get_api_error_eode(self):
"""
使用爬虫爬取 全局错误码
URL的一般格式为: protocol://hostname[:port]/path/[;parameters][?query]#fragment
"""
ir_config = self.env["ir.config_parameter"].sudo()
url = ir_config.get_param("wecom.global_error_code_url")
codes = ir_config.get_param("wecom.global_error_code_item_selection_code")
state = False
msg= ""
try:
_logger.info(_("Start pulling the global error code of WeCom."))
page_text = requests.get(url=url).text
tree = etree.HTML(page_text) # type: ignore
codes_elements = tree.xpath(codes)
methods = []
for code_element in codes_elements:
code_element_str = code_element.xpath("text()")[0]
error_code = code_element_str.split(":", 1)[1:][0]
method_element = code_element.getnext()
method = etree.tostring(method_element, encoding="utf-8", pretty_print=True ).decode() # type: ignore
if " " in error_code:
# 一个元素存在多个错误码
multiple_codes = error_code.split(" ", 1)
for multiple_code in multiple_codes:
multiple_dic = {}
multiple_dic["code"] = multiple_code
multiple_dic["method"] = method
methods.append(multiple_dic)
else:
dic = {}
dic["code"] = error_code
dic["method"] = method
methods.append(dic)
table = tree.xpath("//div[@class='cherry-table-container']/table") # 取出表格
table = etree.tostring(
table[0], encoding="utf-8" # type: ignore
).decode() # 将第一个表格转成string格式
table = table.replace("<th>错误码</th>", "<th>code</th>")
table = table.replace("<th>错误说明</th>", "<th>name</th>")
table = table.replace("<th>排查方法</th>", "<th>method</th>")
df = pd.read_html(table, encoding="utf-8", header=0)[0] # pandas读取table
if 'Unnamed: 3' in df.columns:
del df['Unnamed: 3']
error_results = list(df.T.to_dict().values()) # 转换成列表嵌套字典的格式
errors = []
for index, error in enumerate(error_results):
if (error["code"] == 40054) or (error["code"] == 40055):
error["method"] = self.replaceMethod(str(error["code"]), methods)
elif error["method"] == "查看帮助":
error["method"] = self.replaceMethod(str(error["code"]), methods)
error["sequence"] = index
errors.append(error)
except Exception as e:
msg = _("Failed to pull WeCom global error code, reason:%s") % str(e)
_logger.warning(msg)
state = False
else:
# 写入到odoo
for error in errors:
res = self.search(
[("code", "=", error["code"])],
limit=1,
)
if not res:
self.sudo().create(
{
"code": error["code"],
"name": error["name"],
"method": error["method"],
"sequence": error["sequence"],
}
)
else:
res.sudo().write( # type: ignore
{
"name": error["name"],
"method": error["method"],
"sequence": error["sequence"],
}
)
state = True
msg = _("Successfully pulled the WeCom global error code!")
_logger.info(msg)
finally:
return {"state": state, "msg": msg}
def replaceMethod(self, code, methods):
"""
替换 排查方法
"""
df = pd.DataFrame(methods)
method = df["method"][df["code"] == code].to_string(
index=False
) # 取 包含指定code 值的 "method"列
if method[-2:] == "\\n":
method = method[:-2] # 去掉最后的换行符
return method
def getMiddleStr(self, content, startStr, endStr):
startIndex = content.index(startStr)
if startIndex >= 0:
startIndex += len(startStr)
endIndex = content.index(endStr)
return content[startIndex:endIndex]
-67
View File
@@ -1,67 +0,0 @@
# -*- coding: utf-8 -*-
import re
from odoo import api, fields, models, SUPERUSER_ID, _
class WecomServerApiList(models.Model):
_name = "wecom.service_api_list"
_description = "Wecom Server API List"
_order = "sequence,type"
type = fields.Selection(
[
("1", "Base"), # 基础
("2", "Contacts"), # 通讯录,
("3", "Customer contact"), # 客户联系
("4", "Wechat customer service"), # 微信客服
("5", "Identity authentication"), # 身份验证
("6", "Application management"), # 应用管理
("7", "Message push"), # 消息推送
("8", "Media material"), # 媒体素材
("9", "OA"), # OA
("10", "Efficiency tools"), # 效率工具
("11", "externalpay"), # 企业支付
("12", "Corp Group"), # 企业互联
("13", "Corp Chain"), # 企业链,上下游
("14", "Session content archiving"), # 会话内容存档
("15", "Electronic invoice"), # 电子发票
("16", "School"), # 家校沟通
("17", "School Apps"), # 家校应用
("18", "Report"), # 政民沟通
],
string="Api Type",
required=True,
default="GET",
)
name = fields.Char("Request Name", required=True,)
function_name = fields.Char("Request Function Name", required=True, readonly=True,)
short_url = fields.Char("Request Short Url", required=True,)
request_type = fields.Selection(
[("GET", "GET"), ("POST", "POST"),],
string="Api Request Type",
required=True,
default="GET",
)
description = fields.Html(string="Description")
sequence = fields.Integer(default=0)
_sql_constraints = [
("function_name_uniq", "unique (function_name)", "The function is unique !",),
]
def get_server_api_call(self, function_name):
"""
根据函数名称获取 企业微信API的路由和请求方式
:param function_name : 函数名称
:returns 企业微信API的路由和请求方式的集合
"""
# ['/cgi-bin/gettoken', 'GET']
data = []
res = self.search([("function_name", "=", function_name)], limit=1,)
data.append(res.short_url) # type: ignore
data.append(res.request_type) # type: ignore
return data
-7
View File
@@ -1,7 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
wecom_service_api_access_right,access.wecom.service_api,model_wecom_service_api,base.group_erp_manager,1,1,1,1
wecom_service_api_access_right_user,access.wecom.service_api,model_wecom_service_api,base.group_user,1,0,0,0
wecom_service_api_error_access_right,access.wecom.service_api_error,model_wecom_service_api_error,base.group_erp_manager,1,1,1,1
wecom_service_api_error_access_right_user,access.wecom.service_api_error,model_wecom_service_api_error,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 wecom_service_api_access_right access.wecom.service_api model_wecom_service_api base.group_erp_manager 1 1 1 1
3 wecom_service_api_access_right_user access.wecom.service_api model_wecom_service_api base.group_user 1 0 0 0
4 wecom_service_api_error_access_right access.wecom.service_api_error model_wecom_service_api_error base.group_erp_manager 1 1 1 1
5 wecom_service_api_error_access_right_user access.wecom.service_api_error model_wecom_service_api_error base.group_user 1 0 0 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

-15
View File
@@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
from . import action
from . import file
from . import string
from . import datetime
from . import message
from . import security
from . import data
from . import convert
from . import dictionary
from . import wx_tools
-125
View File
@@ -1,125 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, _
import logging
_logger = logging.getLogger(__name__)
class WecomApiToolsAction(models.AbstractModel):
_name = "wecomapi.tools.action"
_description = "Wecom API Tools - Action"
def ApiExceptionDialog(
self,
ex,
raise_exception=False,
):
"""
API 错误弹框
"""
ir_config = self.env["ir.config_parameter"].sudo()
debug = ir_config.get_param("wecom.debug_enabled")
error = self.env["wecom.service_api_error"].get_error_by_code(ex.errCode)
if debug:
_logger.warning(
_("Wecom API error: %s, error name: %s, error message: %s")
% (str(ex.errCode), error["name"], ex.errMsg)
)
if raise_exception:
params = {
"title": _("Failed"),
"message": _(
"Error code: %s,Error description: %s,Error troubleshooting method: %s,Error Details:%s"
)
% (
str(ex.errCode),
str(error["name"]),
error["method"],
ex.errMsg,
),
"size": "md", # medium
}
action = {
"type": "ir.actions.client",
"tag": "api_error_dialog",
"params": {
"title": params["title"],
"size": params["size"],
"code": str(ex.errCode),
"description": str(error["name"]),
"method": str(error["method"]),
"details": ex.errMsg,
},
}
return action
def WecomInfoNotification(self, msg):
"""
API 提示信息
"""
action = {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": msg["title"],
"type": "info",
"message": msg["message"],
"sticky": msg["sticky"],
},
}
return action
def WecomSuccessNotification(self, msg):
"""
API 成功提示信息
"""
params = {
"title": msg["title"],
"type": "success",
"message": msg["message"],
"sticky": msg["sticky"],
}
if "next" in msg:
params.update({"next": msg["next"]})
action = {
"type": "ir.actions.client",
"tag": "display_notification",
"params": params,
}
return action
def WecomWarningNotification(self, msg):
"""
API 警告提示信息
"""
action = {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": msg["title"],
"type": "warning",
"message": msg["message"],
"sticky": msg["sticky"],
},
}
return action
def WecomErrorNotification(self, msg):
"""
API 错误提示信息
"""
action = {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": msg["title"],
"type": "error",
"message": msg["message"],
"sticky": msg["sticky"],
},
}
return action
-47
View File
@@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
import html2text
from odoo import api, models, tools, _
class WecomApiToolsTypeConvert(models.AbstractModel):
_name = "wecomapi.tools.convert"
_description = "Wecom API Tools - Type convert"
def html2text_handle(self, body_html):
# 转换markdown格式
if bool(body_html):
return html2text.html2text(body_html)
else:
return None
def str2bool(self):
"""
字符串转布尔值
:param val: 字符串
:return: 布尔值
"""
# return self.value.lower() in ("yes", "true", "t", "1")
if self.value.lower() in ["true", "t", "1"]: # type: ignore
return True
elif self.value.lower() in ["false", "f", "0"]: # type: ignore
return False
else:
return False
def sex2gender(self, sex):
"""
性别转换
"""
if sex == "1":
return "male"
elif sex == "2":
return "female"
else:
return "other"
def gendge2sex(self, gender):
if gender == "male":
return "1"
elif gender == "female":
return "2"
-42
View File
@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, models, tools, _
import logging
_logger = logging.getLogger(__name__)
class WecomApiToolsData(models.AbstractModel):
_name = "wecomapi.tools.data"
_description = "Wecom API Tools - Data"
def union_data_set(self, set1, set2):
"""
2个数据集的并集
:param set1: 数据集1
:param set2: 数据集2
:returns: 并集 的
"""
# set1.union(set2)
return set1 | set2
def difference_data_set(self, set1, set2):
"""
2个数据集的差集
:param set1: 数据集1
:param set2: 数据集2
:returns: 差集
"""
# set1.difference(set2)
return set1 - set2
def intersection_data_set(self, set1, set2):
"""
2个数据集的交集
:param set1: 数据集1
:param set2: 数据集2
:returns: 交集
"""
# set1.intersection(set2)
return set1 & set2
-82
View File
@@ -1,82 +0,0 @@
# -*- coding: utf-8 -*-
import time
from odoo import api, models, tools, _
from datetime import datetime, timedelta
import logging
_logger = logging.getLogger(__name__)
class WecomApiToolsDatetime(models.AbstractModel):
_name = "wecomapi.tools.datetime"
_description = "Wecom API Tools - Datetime"
def timestamp2datetime(self, time_stamp):
"""
时间戳转日期时间
"""
if len(str(time_stamp)) > 10:
# 一般爬取下来的时间戳长度都是13位的数字,而time.localtime的参数要的长度是10位,所以我们需要将其/1000并取整即可
time_stamp = int(time_stamp / 1000)
loc_time = time.localtime(time_stamp)
return time.strftime("%Y-%m-%d %H:%M:%S", loc_time)
def cheeck_days_overdue(self, datetime_start, maxday):
"""[summary]
检查天数是否超期
Args:
datetime_start_str ([type]): [description]
datetime_end_str ([type]): [description]
maxday ([type]): [description] 天数
Returns:
True: 超期
False:未超期
"""
now = datetime.now()
# print(now - datetime_start)
if datetime_start > (now - timedelta(days=maxday)):
return False
else:
return True
def cheeck_hours_overdue(self, datetime_start, maxhour):
"""[summary]
检查小时是否超期
Args:
datetime_start_str ([type]): [description]
datetime_end_str ([type]): [description]
maxhour ([type]): [description] 小时
Returns:
True: 超期
False:未超期
"""
now = datetime.now()
if datetime_start > (now - timedelta(hours=maxhour)):
return False
else:
return True
def cheeck_minutes_overdue(self, datetime_start, maxminute):
"""[summary]
检查分钟是否超期
Args:
datetime_start_str ([type]): [description]
datetime_end_str ([type]): [description]
maxminute ([type]): [description] 分钟
Returns:
True: 超期
False:未超期
"""
now = datetime.now()
if datetime_start > (now - timedelta(minutes=maxminute)):
return False
else:
return True
-18
View File
@@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, models, tools, _
class WecomApiToolsDictionary(models.AbstractModel):
_name = "wecomapi.tools.dictionary"
_description = "Wecom API Tools - Dictionary"
def check_dictionary_keywords(self, dictionary, key):
"""
检查字典中是否存在key
"""
# dictionary, key = (self.value[0], self.value[1])
if key in dictionary.keys():
return dictionary[key]
else:
return None
-52
View File
@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, models, tools, _
import base64
import requests
import io
import os
import platform
import logging
from odoo.modules.module import get_module_resource
_logger = logging.getLogger(__name__)
class WecomApiToolsFile(models.AbstractModel):
_name = "wecomapi.tools.file"
_description = "Wecom API Tools - File"
def path_is_exists(self, path, subpath):
"""
检文件夹路径是否存在,不存在则创建路径
return:返回路径
"""
if platform.system() == "Windows":
filepath = path.replace("\\", "/") + subpath + "/"
else:
filepath = path + subpath + "/"
# print(filepath)
if not os.path.exists(filepath):
os.makedirs(filepath)
return filepath
def get_avatar_base64(self, use_default_avatar, gender, avatar_url):
"""
获取企业微信用户头像的base64编码
return:返回base64
"""
imgbase64 = ""
if use_default_avatar or avatar_url == "":
image_name = "default_image.png"
if gender == "1":
image_name = "default_male_image.png"
elif gender == "2":
image_name = "default_female_image.png"
default_image = get_module_resource("hrms_base", "static/src/img", image_name)
# print(default_image)
with open(default_image, "rb") as f:
imgbase64 = base64.b64encode(f.read())
else:
imgbase64 = base64.b64encode(requests.get(avatar_url).content)
return imgbase64
-14
View File
@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, models, tools, _
import logging
_logger = logging.getLogger(__name__)
class WecomApiToolsMessage(models.AbstractModel):
_name = "wecomapi.tools.message"
_description = "Wecom API Tools - Message"
def message_split(self,text):
""" """
-83
View File
@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
import logging
import hashlib
import random
import hashlib
from Crypto.Cipher import AES
from passlib.context import CryptContext
import xml.etree.cElementTree as ET
import hashlib
_logger = logging.getLogger(__name__)
NUMLIST = ["0","1","2","3","4","5","6","7","8","9","q","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","W","R","S","T","U","V","W","X","Y","Z",]
class WecomApiToolsSecurity(models.AbstractModel):
_name = "wecomapi.tools.security"
_description = "Wecom API Tools - Security"
def random_str(self, num):
"""
生成随机字符串
:return:
"""
rang = num
if rang == None:
random_str = "".join(random.choice(NUMLIST) for i in range(8))
else:
random_str = "".join(random.choice(NUMLIST) for i in range(int(rang)))
return random_str
def random_passwd(self, num):
"""
生成随机密码
:return:
"""
rang = num
if rang == None:
passwd = "".join(random.choice(NUMLIST) for i in range(8))
else:
passwd = "".join(random.choice(NUMLIST) for i in range(int(rang)))
crypt_context = CryptContext(
schemes=["pbkdf2_sha512", "plaintext"], deprecated=["plaintext"]
)
hash_password = (
crypt_context.hash
if hasattr(crypt_context, "hash")
else crypt_context.encrypt
)
return hash_password(passwd)
def generate_jsapi_signature(self, jsapi_ticket, nonceStr, timestamp):
"""
使用sha1加密算法,生成JSAPI的签名
------------------------------
company: 公司
nonceStr: 生成签名的随机串
timestamp: 生成签名的时间戳
url: 当前网页的URL, 不包含#及其后面部分
"""
# 生成签名前,刷新 ticke .search([(("is_wecom_organization", "=", True))])
# company.sudo().get_jsapi_ticket()
ICP = self.env["ir.config_parameter"].sudo()
web_base_url = ICP.get_param("web.base.url", default="http://localhost:8069")
if web_base_url[-1] == "/":
web_base_url = web_base_url + "web"
else:
web_base_url = web_base_url + "/web"
jsapi_ticket_str = ("jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s") % (
jsapi_ticket,
nonceStr,
timestamp,
web_base_url,
)
encrypts = hashlib.sha1(jsapi_ticket_str.encode("utf-8")).hexdigest()
return encrypts
-42
View File
@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
import random
from odoo import api, models, tools, _
import logging
_logger = logging.getLogger(__name__)
class WecomApiToolsString(models.AbstractModel):
_name = "wecomapi.tools.string"
_description = "Wecom API Tools - Fields"
def str2bool(self):
"""
字符串转布尔值
:param val: 字符串
:return: 布尔值
"""
# return self.value.lower() in ("yes", "true", "t", "1")
if self.value.lower() in ["true", "t", "1"]: # type: ignore
return True
elif self.value.lower() in ["false", "f", "0"]: # type: ignore
return False
else:
return False
# def int2bool(self):
# """
# 整形转布尔值
# :param val: 整形
# :return: 布尔值
# """
# if self.value.lower() in ["true", "t", "1"]:
# return True
# elif self.value.lower() in ["false", "f", "0"]:
# return False
# else:
# return False
-105
View File
@@ -1,105 +0,0 @@
# -*- coding: utf-8 -*-
from PIL import Image
import os
import base64
import random
import html2text
import platform
import hashlib
from passlib.context import CryptContext
from odoo import api, models, tools, _
from odoo.modules.module import get_module_resource
from datetime import datetime, timedelta
import logging
_logger = logging.getLogger(__name__)
class WxTools(models.AbstractModel):
_name = "wecom.tools"
_description = "Wecom Tools"
def recipients_split(text): # type: ignore
"""
使用 | 拆分企业微信消息的接收对象
"""
if not text:
return []
def wecom_user_enable(self):
"""
企业微信用户是否启用
:param value:
:return:
"""
if self.value == "0": # type: ignore
self.result = False
if self.value == "1": # type: ignore
self.result = True
return self.result
def encode_avatar_image_as_base64(self, gender):
if gender == "1":
default_image = get_module_resource(
"wecom_api", "static/src/img", "default_male_image.png"
)
elif gender == "2":
default_image = get_module_resource(
"wecom_api",
"static/src/img",
"default_female_image.png",
)
else:
default_image = get_module_resource(
"wecom_api", "static/src/img", "default_image.png"
)
with open(default_image, "rb") as f:
return base64.b64encode(f.read())
def get_default_avatar_url(self, gender):
params = self.env["ir.config_parameter"].sudo()
base_url = params.get_param("web.base.url")
if gender == "1":
default_image_url = (
base_url + "/wecom_api/static/src/img/default_male_image.png"
)
elif gender == "2":
default_image_url = (
base_url + "/wecom_api/static/src/img/default_female_image.png"
)
return default_image_url # type: ignore
def encode_image_as_base64(self):
if not self.value: # type: ignore
pass
else:
with open(self.value, "rb") as f: # type: ignore
encoded_string = base64.b64encode(f.read())
return encoded_string
def is_exists(self):
"""
判断是否存在值
:return:
"""
if not self.value: # type: ignore
self.result = False
else:
self.result = True
return self.result
def mail_is_exists(self):
"""
判断是否存在值
:return:
"""
if not self.value: # type: ignore
self.result = ""
else:
self.result = self.value # type: ignore
return self.result
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from . import models
from . import controllers
-42
View File
@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
{
"name": "WeCom Authentication",
"author": "RStudio",
"website": "https://gitee.com/rainbowstudio/wecom",
"sequence": 605,
"installable": True,
"application": True,
"auto_install": False,
"category": "WeCom Suites/Authentication",
"version": "17.0.0.1",
"summary": """
One click login, code scanning login.
""",
"description": """
""",
"depends": [
"portal",
"auth_oauth",
"wecom_material",
],
"data": [
"data/wecom_apps_data.xml",
"data/wecom_app_config_data.xml",
"data/wecom_oauth_data.xml",
"views/res_config_settings_views.xml",
"views/auth_oauth_views.xml",
# "views/wecom_apps_views.xml",
# "views/auth_signup_login_templates.xml",
],
"assets": {
"web.assets_backend": [
# "wecom_auth_oauth/static/src/webclient/**/*",
],
"web.assets_frontend": [
"wecom_auth_oauth/static/src/js/wecom_login/*",
],
},
"bootstrap": True, # 加载登录屏幕的翻译,
"license": "Other proprietary",
}
-3
View File
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import main
-110
View File
@@ -1,110 +0,0 @@
# -*- coding: utf-8 -*-
# !参考 \addons\auth_oauth\controllers\main.py
import base64
import functools
import json
import logging
import os
import werkzeug.urls
import werkzeug.utils
from werkzeug.exceptions import BadRequest
from odoo import api, http, SUPERUSER_ID, _
from odoo.exceptions import AccessDenied
from odoo.http import request, Response
from odoo import registry as registry_get
from odoo.tools.misc import clean_context
from odoo.addons.auth_signup.controllers.main import AuthSignupHome as Home
from odoo.addons.web.controllers.utils import ensure_db, _get_login_redirect_url
_logger = logging.getLogger(__name__)
WECOM_BROWSER_MESSAGES = {
"not_wecom_browser": _(
"The current browser is not an WeCom built-in browser, so the one-click login function cannot be used."
),
"is_wecom_browser": _(
"It is detected that the page is opened in the built-in browser of WeCom, please select company."
),
}
class WeComOAuthLogin(Home):
def list_providers(self):
providers = super(WeComOAuthLogin, self).list_providers()
for provider in providers:
if (
"login.work.weixin.qq.com/wwlogin/sso/login"
in provider["auth_endpoint"]
):
# 链接格式
# https://login.work.weixin.qq.com/wwlogin/sso/login?login_type=LOGIN_TYPE&appid=APPID&redirect_uri=REDIRECT_URI&state=STATE
return_url = request.httprequest.url_root + "wecom/authorize"
state = self.get_state(provider)
params = dict(
appid=False,
redirect_uri=return_url,
response_type="code",
scope=provider["scope"],
state=json.dumps(state),
)
provider["auth_link"] = "%s?%s%s" % (
provider["auth_endpoint"],
werkzeug.urls.url_encode(params), # type: ignore
"#wechat_redirect",
)
return providers
class OAuthController(http.Controller):
@http.route("/wwcom_login_info", type="json", auth="none")
def wecom_get_login_info(self, **kwargs):
data = {}
if "is_wecom_browser" in kwargs:
if kwargs["is_wecom_browser"]:
data = {
"is_wecom_browser": True,
"msg": WECOM_BROWSER_MESSAGES["is_wecom_browser"],
"companies": [],
}
else:
data = {
"is_wecom_browser": False,
"msg": WECOM_BROWSER_MESSAGES["not_wecom_browser"],
"companies": [],
}
else:
data = {
"join_button_name": _("Join WeCom,Become our employee."),
"companies": [],
}
# 获取 标记为 企业微信组织 的公司
companies = request.env["res.company"].search( # type: ignore
[(("is_wecom_organization", "=", True))]
)
if len(companies) > 0:
for company in companies:
# app_config = request.env["wecom.app_config"].sudo()
# contacts_app = company.contacts_app_id.sudo() # 通讯录应用
auth_app = company.auth_app_id.sudo() # 验证登录应用
data["companies"].append(
{
"id": company["id"],
"name": company["abbreviated_name"],
"fullname": company["name"],
"appid": company["corpid"],
"agentid": auth_app.agentid if auth_app else 0,
"enabled_join": company["wecom_contacts_join_qrcode_enabled"],
"join_qrcode": company["wecom_contacts_join_qrcode"],
}
)
return data
@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- 1 -->
<record id="wecom_app_config_authentication_auth_redirect_uri" model="wecom.app_config">
<field name="app_id" ref="wecom_contacts.wecom_app_first_self_app"/>
<field name="name">Callback link address redirected after authorization</field>
<field name="key">auth_redirect_uri</field>
<field name="ttype">char</field>
<field name="value">http://xxx.xxx.com:port/wecom_auth_oauth/authorize</field>
<field name="description" type="html">
<p class="o_view_nocontent_smiling_face">
Fill in the "Corp ID", "AgentId" and "Secret" that require authentication and authorization, and use the domain name to access,
<span style="color:orange;">
Click the button "One-click to set up WeCom OAuth"
</span>
</p>
</field>
</record>
<!-- 2 -->
<record id="wecom_app_config_authentication_qr_redirect_uri" model="wecom.app_config">
<field name="app_id" ref="wecom_contacts.wecom_app_first_self_app"/>
<field name="name">Scan the QR code to log in and call back the link address</field>
<field name="key">auth_qr_redirect_uri</field>
<field name="ttype">char</field>
<field name="value">http://xxx.xxx.com:port/wecom_auth_oauth/qr</field>
<field name="description" type="html">
<p class="o_view_nocontent_smiling_face">
Fill in the "Corp ID", "AgentId" and "Secret" that require authentication and authorization, and use the domain name to access,
<span style="color:orange;">
Click the button "One-click to set up WeCom OAuth"
</span>
</p>
</field>
</record>
</data>
</odoo>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- 应用子类型 -->
<!-- <record id="wecom_app_subtype_auth" model="wecom.app.subtype">
<field name="name">Authentication</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_self"/>
<field name="code">auth</field>
<field name="sequence">1</field>
</record> -->
<!-- 应用 -->
<record id="wecom_contacts.wecom_app_first_self_app" model="wecom.apps">
<field name="subtype_ids" eval="[(4,ref('wecom_base.wecom_app_subtype_auth'))]"/>
</record>
</data>
</odoo>
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<function model="auth.oauth.provider" name="write">
<value eval="[ref('auth_oauth.provider_openerp')]"/>
<value eval="{'enabled': False}"/>
</function>
<record id="provider_wecom_quick_login" model="auth.oauth.provider">
<field name="name">WeCom Quick Login</field>
<!--身份验证网址-->
<field name="auth_endpoint">https://login.work.weixin.qq.com/wwlogin/sso/login</field>
<!--作用域-->
<field name="scope">snsapi_base</field>
<!--验证网址-->
<field name="validation_endpoint">https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo</field>
<!--数据网址-->
<field name="data_endpoint">https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail</field>
<field name="css_class">fa fa-fw fa-wechat wecom_auth_quick</field>
<field name="body">WeCom quick login</field>
<field name="enabled" eval="False"/>
<field name="is_woecom" eval="False"/>
<field name="description"></field>
</record>
<!-- <record id="provider_wecom_qrconnect" model="auth.oauth.provider">
<field name="name">WeCom OAuth2 Scan</field>
<field name="auth_endpoint">https://open.work.weixin.qq.com/wwopen/sso/qrConnect</field>
<field name="scope"></field>
<field name="validation_endpoint"></field>
<field name="data_endpoint"></field>
<field name="css_class">fa fa-fw fa-qrcode wecom_auth_scancode</field>
<field name="body">WeCom scan code login</field>
<field name="enabled" eval="False"/>
<field name="description"></field>
</record> -->
</data>
</odoo>
-287
View File
@@ -1,287 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * wecom_auth_oauth
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-12 09:11+0000\n"
"PO-Revision-Date: 2024-03-12 17:20+0800\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.4.2\n"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "<i class=\"fa fa-cogs fa-lg\" aria-label=\"Authentication Configuration\"/>"
msgstr "<i class=\"fa fa-cogs fa-lg\" aria-label=\"身份认证配置\"/>"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "<i class=\"fa fa-key fa-lg\" aria-label=\"Auth Secret\"/>"
msgstr "<i class=\"fa fa-key fa-lg\" aria-label=\"身份认证密钥\"/>"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "<i class=\"fa fa-list-ol fa-lg\" aria-label=\"Auth AgentId\"/>"
msgstr "<i class=\"fa fa-list-ol fa-lg\" aria-label=\"身份认证应用Id\"/>"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid ""
"<i class=\"fa fa-puzzle-piece fa-lg\" aria-label=\"Authentication Application\"/>"
msgstr "<i class=\"fa fa-puzzle-piece fa-lg\" aria-label=\"身份认证应用\"/>"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "<i class=\"fa fa-ticket fa-lg\" role=\"img\" aria-label=\"JSAPI Ticket\"/>"
msgstr ""
"<i class=\"fa fa-ticket fa-lg\" role=\"img\" aria-label=\"JSAPI临时凭证\"/>"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "<strong>Save</strong> this page and come back here to set up other feature."
msgstr "<strong>保存</strong> 此页面并返回此处以设置其它功能。"
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_res_config_settings__auth_agentid
msgid "Agent ID"
msgstr "应用ID"
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_res_company__auth_app_id
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_res_config_settings__auth_app_id
msgid "Application"
msgstr "应用"
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_res_config_settings__auth_app_config_ids
msgid "Application Configuration"
msgstr "应用配置"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid ""
"Application Management→Application→Open the application that needs to be "
"verified and authorized, copy 'AgentId'"
msgstr "应用程序管理→应用→打开需要验证和授权的应用程序,复制“AgentId”"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid ""
"Application Management→Application→Open the application that needs to be "
"verified and authorized, copy 'Secret'"
msgstr "应用程序管理→应用→打开需要验证和授权的应用程序,复制“Secret”"
#. module: wecom_auth_oauth
#. odoo-python
#: code:addons/wecom_auth_oauth/models/res_config_settings.py:0
#, python-format
msgid "Auth application ID and secret cannot be empty!"
msgstr "身份验证应用ID和Secret 不能为空!"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "Binding Auth Application"
msgstr "绑定验证应用"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "Button group"
msgstr "按钮组"
#. module: wecom_auth_oauth
#: model:wecom.app_config,name:wecom_auth_oauth.wecom_app_config_authentication_auth_redirect_uri
msgid "Callback link address redirected after authorization"
msgstr ""
#. module: wecom_auth_oauth
#: model:ir.model,name:wecom_auth_oauth.model_res_company
msgid "Companies"
msgstr "公司"
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_res_config_settings__company_id
msgid "Company"
msgstr "公司"
#. module: wecom_auth_oauth
#: model:ir.model,name:wecom_auth_oauth.model_res_config_settings
msgid "Config Settings"
msgstr "配置设置"
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_auth_oauth_provider__description
msgid "Description"
msgstr "说明"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "Enable WeCom quick login"
msgstr "启用企微快速登录"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "Expiration time"
msgstr "过期时间"
#. module: wecom_auth_oauth
#: model_terms:wecom.app_config,description:wecom_auth_oauth.wecom_app_config_authentication_auth_redirect_uri
#: model_terms:wecom.app_config,description:wecom_auth_oauth.wecom_app_config_authentication_qr_redirect_uri
msgid ""
"Fill in the \"Corp ID\", \"AgentId\" and \"Secret\" that require authentication "
"and authorization, and use the domain name to access,\n"
" <span style=\"color:orange;\">\n"
" Click the button \"One-click to set up WeCom OAuth\"\n"
" </span>"
msgstr ""
"在需要认证和授权的“企业ID”、“AgentId”和“Secret”中,使用域名进行访问,\n"
" <span style=\"color:orange;\">\n"
" Click the button \"One-click to set up WeCom OAuth\"\n"
" </span>"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "Get JSAPI Ticket"
msgstr "获取JSAPI临时凭证"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "Get application info"
msgstr "获取应用信息"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid ""
"Is a temporary ticket for the H5 application to call the Wecom JS interface. "
"Under normal circumstances, <b>the validity period of jsapi_ticket is 7200 "
"seconds</b>, obtained by access_token. Since the number of API calls to the "
"jsapi_ticket is very limited (an enterprise can get up to 400 times in an hour, "
"and a single application cannot get more than 100 times), frequent refresh "
"jsapi_ticket will cause the API call to be limited, affecting their own "
"business, <b>and developers must cache jsapi_ticket in their own service global "
"cache</b>."
msgstr ""
"jsapi_ticket是H5应用调用企业微信JS接口的临时票据。正常情况下,<b>jsapi_ticket的有"
"效期为7200秒</b>,通过access_token获得。由于API调用jsapi_ticket的次数非常有限(一"
"小时内,一个企业最多可获取400次,且单个应用不能超过100次),频繁刷新jsapi_ticket"
"会导致api调用受限,影响自身业务,<b>开发者必须在自己的服务全局缓存应用的"
"jsapi_ticket</b>中。"
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_res_config_settings__auth_app_jsapi_ticket
msgid "JSAPI Ticket"
msgstr "JSAPI临时凭证"
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_res_config_settings__auth_app_jsapi_ticket_expiration_time
msgid "JSAPI Ticket Expiration Time"
msgstr "JSAPI临时凭证有效时间"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "Management application"
msgstr "管理应用"
#. module: wecom_auth_oauth
#: model:ir.model,name:wecom_auth_oauth.model_auth_oauth_provider
msgid "OAuth2 provider"
msgstr "OAuth2服务商"
#. module: wecom_auth_oauth
#. odoo-python
#: code:addons/wecom_auth_oauth/models/wecom_apps.py:0
#, python-format
msgid "Please click on the 'Generate Parameters' button!"
msgstr ""
#. module: wecom_auth_oauth
#: model:wecom.app_config,name:wecom_auth_oauth.wecom_app_config_authentication_qr_redirect_uri
msgid "Scan the QR code to log in and call back the link address"
msgstr ""
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_res_config_settings__auth_secret
msgid "Secret"
msgstr "密钥"
#. module: wecom_auth_oauth
#. odoo-python
#: code:addons/wecom_auth_oauth/models/res_config_settings.py:0
#, python-format
msgid "Start getting ticket for company [%s]"
msgstr ""
#. module: wecom_auth_oauth
#. odoo-python
#: code:addons/wecom_auth_oauth/models/res_config_settings.py:0
#, python-format
msgid "Successfully obtained ticket!"
msgstr ""
#. module: wecom_auth_oauth
#. odoo-python
#: code:addons/wecom_auth_oauth/models/wecom_apps.py:0
#, python-format
msgid ""
"The parameter \"Callback link address redirected after authorization\" does not "
"exist!\n"
"Please click on the 'Generate Parameters' button!\n"
" "
msgstr ""
#. module: wecom_auth_oauth
#. odoo-python
#: code:addons/wecom_auth_oauth/models/wecom_apps.py:0
#, python-format
msgid ""
"The parameter \"Scan the QR code to log in and call back the link address\" does "
"not exist!\n"
"Please click on the 'Generate Parameters' button!\n"
" "
msgstr ""
#. module: wecom_auth_oauth
#. odoo-python
#: code:addons/wecom_auth_oauth/models/res_config_settings.py:0
#, python-format
msgid "Ticket is still valid, and no update is required!"
msgstr "临时凭证仍然有效,不需要更新!"
#. module: wecom_auth_oauth
#. odoo-python
#: code:addons/wecom_auth_oauth/models/res_config_settings.py:0
#, python-format
msgid "Tips"
msgstr "提示"
#. module: wecom_auth_oauth
#: model:ir.model,name:wecom_auth_oauth.model_res_users
msgid "User"
msgstr "用户"
#. module: wecom_auth_oauth
#: model_terms:ir.ui.view,arch_db:wecom_auth_oauth.res_config_settings_view_form
msgid "View OAuth providers"
msgstr "查看OAuth提供程序"
#. module: wecom_auth_oauth
#: model:ir.model.fields,field_description:wecom_auth_oauth.field_auth_oauth_provider__is_woecom
msgid "WeCom OAuth"
msgstr "企业微信OAuth"
#. module: wecom_auth_oauth
#: model:auth.oauth.provider,body:wecom_auth_oauth.provider_wecom_quick_login
msgid "WeCom quick login"
msgstr "企微快速登录"
#. module: wecom_auth_oauth
#: model:ir.model,name:wecom_auth_oauth.model_wecom_apps
msgid "Wecom Application"
msgstr "企微应用"
-8
View File
@@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
from . import res_company
from . import res_config_settings
from . import auth_oauth
from . import wecom_apps
from . import res_users
-10
View File
@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class AuthOAuthProvider(models.Model):
_inherit = "auth.oauth.provider"
is_woecom = fields.Boolean(string="WeCom OAuth", default=False)
description = fields.Html(string="Description", translate=True)
-82
View File
@@ -1,82 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.addons.wecom_api.api.wecom_abstract_api import ApiException # type: ignore
import werkzeug.urls
import werkzeug.utils
import urllib
import datetime
import logging
_logger = logging.getLogger(__name__)
class Company(models.Model):
_inherit = "res.company"
auth_app_id = fields.Many2one(
"wecom.apps",
string="Application",
# required=True,
# default=lambda self: self.env.company,
# domain="[('company_id', '=', current_company_id)]",
domain="[('company_id', '=', current_company_id)]",
)
def set_oauth_provider_wxwork(self):
web_base_url = self.env["ir.config_parameter"].get_param("web.base.url")
new_auth_redirect_uri = (
urllib.parse.urlparse(web_base_url).scheme # type: ignore
+ "://"
+ urllib.parse.urlparse(web_base_url).netloc # type: ignore
+ urllib.parse.urlparse(self.auth_redirect_uri).path # type: ignore
)
new_qr_redirect_uri = (
urllib.parse.urlparse(web_base_url).scheme # type: ignore
+ "://"
+ urllib.parse.urlparse(web_base_url).netloc # type: ignore
+ urllib.parse.urlparse(self.qr_redirect_uri).path # type: ignore
)
# 设置回调链接地址
self.auth_redirect_uri = new_auth_redirect_uri
self.qr_redirect_uri = new_qr_redirect_uri
auth_endpoint = "https://open.weixin.qq.com/connect/oauth2/authorize"
qr_auth_endpoint = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect"
try:
providers = (
self.env["auth.oauth.provider"]
.sudo()
.search(
[
"|",
("enabled", "=", True),
("enabled", "=", False),
]
)
)
except Exception:
providers = []
for provider in providers:
if auth_endpoint in provider["auth_endpoint"]:
provider.write(
{
# "client_id": client_id,
"validation_endpoint": self.auth_redirect_uri,
"enabled": True,
}
)
if qr_auth_endpoint in provider["auth_endpoint"]:
provider.write(
{
# "client_id": client_id,
"validation_endpoint": self.qr_redirect_uri,
"enabled": True,
}
)
@@ -1,149 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from odoo.addons.wecom_api.api.wecom_abstract_api import ApiException # type: ignore
import werkzeug.urls
import werkzeug.utils
import urllib
import datetime
import logging
_logger = logging.getLogger(__name__)
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
company_id = fields.Many2one(
"res.company",
string="Company",
required=True,
default=lambda self: self.env.company,
)
auth_app_id = fields.Many2one(
related="company_id.auth_app_id",
readonly=False,
)
auth_agentid = fields.Integer(related="auth_app_id.agentid", readonly=False)
auth_secret = fields.Char(related="auth_app_id.secret", readonly=False)
auth_app_jsapi_ticket = fields.Char(
related="auth_app_id.jsapi_ticket", readonly=False
)
auth_app_jsapi_ticket_expiration_time = fields.Datetime(
related="auth_app_id.jsapi_ticket_expiration_time", readonly=False
)
auth_app_config_ids = fields.One2many(
# related="company_id.contacts_auto_sync_hr_enabled", readonly=False
related="auth_app_id.app_config_ids",
# domain="[('company_id', '=', company_id),('app_id', '=', contacts_app_id)]",
readonly=False,
)
def enable_wecom_quick_login(self):
ir_model_data = self.env["ir.model.data"]
provider_wecom_quick_login = ir_model_data._xmlid_lookup(
"wecom_auth_oauth.provider_wecom_quick_login",
)[1]
providers = [provider_wecom_quick_login]
for provider in providers:
p = self.env["auth.oauth.provider"].search([("id", "=", provider)], limit=1)
if p:
p.enabled = True
def set_oauth_provider_wecom(self):
self.auth_app_id.set_oauth_provider_wecom() # type: ignore
def generate_parameters(self):
"""
生成参数
:return:
"""
code = self.env.context.get("code")
if bool(code) and code == "auth":
for record in self:
# if not record.contacts_app_id:
# raise ValidationError(_("Please bind contact app!"))
# else:
record.auth_app_id.with_context(code=code).generate_parameters() # type: ignore
super(ResConfigSettings, self).generate_parameters() # type: ignore
def get_app_info(self):
"""
获取应用信息
:return:
"""
app = self.env.context.get("app")
for record in self:
if app == "auth" and (
record.auth_app_id.agentid == 0 or record.auth_app_id.secret == "" # type: ignore
):
raise UserError(_("Auth application ID and secret cannot be empty!"))
else:
record.auth_app_id.get_app_info() # type: ignore
super(ResConfigSettings, self).get_app_info() # type: ignore
def get_auth_app_jsapi_ticket(self):
"""
获取身份认证应用的jsapi_ticket
:return:
"""
ir_config = self.env["ir.config_parameter"].sudo()
debug = ir_config.get_param("wecom.debug_enabled")
if debug:
_logger.info(
_("Start getting ticket for company [%s]") % (self.company_id.name) # type: ignore
)
try:
if (
self.auth_app_jsapi_ticket_expiration_time
and self.auth_app_jsapi_ticket_expiration_time > datetime.now()
):
# 未过期,
# print("未过期")
msg = {
"title": _("Tips"),
"message": _("Ticket is still valid, and no update is required!"),
"sticky": False,
}
return self.env["wecomapi.tools.action"].WecomInfoNotification(msg)
else:
# print("过期")
wecom_api = self.env["wecom.service_api"].InitServiceApi(self.company_id.corpid, self.auth_app_id.secret) # type: ignore
response = wecom_api.httpCall(
self.env["wecom.service_api_list"].get_server_api_call(
"AGENT_GET_TICKET"
),
{"type": "agent_config"},
)
# print(response)
except ApiException as ex:
return self.env["wecomapi.tools.action"].ApiExceptionDialog(
ex, raise_exception=True
)
else:
if response["errcode"] == 0:
self.write(
{
"auth_app_jsapi_ticket": response["ticket"],
"auth_app_jsapi_ticket_expiration_time": datetime.now()
+ timedelta(seconds=response["expires_in"]),
}
)
msg = {
"title": _("Tips"),
"message": _("Successfully obtained ticket!"),
"sticky": False,
"next": {
"type": "ir.actions.client",
"tag": "reload",
},
}
return self.env["wecomapi.tools.action"].WecomSuccessNotification(msg)
-59
View File
@@ -1,59 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, api, _
from odoo.exceptions import AccessDenied
class ResUsers(models.Model):
_inherit = "res.users"
# ---------------------
# 验证
# ---------------------
@api.model
def wecom_auth_oauth(self, provider, params):
"""
允许一键登录和扫码登录且标记了企业微信的用户登录系统
:param provider:
:param params:
:return:
"""
wecom_web_auth_endpoint = "https://open.weixin.qq.com/connect/oauth2/authorize"
wecom_qr_auth_endpoint = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect"
wecom_providers = (
self.env["auth.oauth.provider"].sudo().search([("id", "=", provider),])
)
if (
wecom_web_auth_endpoint in wecom_providers["auth_endpoint"]
or wecom_qr_auth_endpoint in wecom_providers["auth_endpoint"]
):
# 扫码登录
oauth_userid = params["UserId"].lower()
oauth_user = self.search(
[
# ("oauth_uid", "=", oauth_userid),
("wecom_userid", "=", oauth_userid),
("is_wecom_user", "=", True),
("active", "=", True),
]
)
if not oauth_user or len(oauth_user) > 1:
return AccessDenied
return (self.env.cr.dbname, oauth_user.login, oauth_userid) # type: ignore
else:
return AccessDenied
def _check_credentials(self, password, env):
# password为企业微信的用户ID
try:
return super(ResUsers, self)._check_credentials(password, env) # type: ignore
except AccessDenied:
res = self.sudo().search(
[("id", "=", self.env.uid), ("wecom_userid", "=", password)]
)
if not res:
raise
-152
View File
@@ -1,152 +0,0 @@
# -*- coding: utf-8 -*-
import logging
import datetime
import werkzeug.utils
import urllib
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class WeComApps(models.Model):
_inherit = "wecom.apps"
def generate_parameters_by_code(self, code):
"""
根据code生成参数
:param code:
:return:
"""
if code == "auth":
ir_model_data = self.env["ir.model.data"]
auth_redirect_uri = ir_model_data.get_object_reference(
"wecom_auth_oauth", "wecom_app_config_authentication_auth_redirect_uri"
)[1]
qr_redirect_uri = ir_model_data.get_object_reference(
"wecom_auth_oauth", "wecom_app_config_authentication_qr_redirect_uri"
)[1]
vals_list = [
auth_redirect_uri,
qr_redirect_uri,
]
for id in vals_list:
app_config_id = self.env["wecom.app_config"].search([("id", "=", id)])
app_config = (
self.env["wecom.app_config"]
.sudo()
.search([("app_id", "=", self.id), ("key", "=", app_config_id.key)]) # type: ignore
)
if not app_config:
app_config = (
self.env["wecom.app_config"]
.sudo()
.create(
{
"name": app_config_id.name,
"app_id": self.id, # type: ignore
"key": app_config_id.key,
"ttype": app_config_id.ttype,
"value": app_config_id.value,
"description": app_config_id.description,
}
)
)
else:
app_config.sudo().write(
{
"name": app_config_id.name,
"value": app_config_id.value,
"description": app_config_id.description,
}
)
super(WeComApps, self).generate_parameters_by_code(code) # type: ignore
# ————————————————————————————————————
# 设置认证应用配置
# ————————————————————————————————————
def set_oauth_provider_wecom(self):
web_base_url = self.env["ir.config_parameter"].get_param("web.base.url")
auth_redirect_uri = None
qr_redirect_uri = None
if len(self.app_config_ids) > 0: # type: ignore
for record in self.app_config_ids: # type: ignore
if record["key"] == "auth_redirect_uri":
auth_redirect_uri = record
else:
raise UserError(
_(
"""The parameter "Callback link address redirected after authorization" does not exist!
Please click on the 'Generate Parameters' button!
"""
)
)
if record["key"] == "qr_redirect_uri":
qr_redirect_uri = record
else:
raise UserError(
_(
"""The parameter "Scan the QR code to log in and call back the link address" does not exist!
Please click on the 'Generate Parameters' button!
"""
)
)
new_auth_redirect_uri = (
urllib.parse.urlparse(web_base_url).scheme # type: ignore
+ "://"
+ urllib.parse.urlparse(web_base_url).netloc # type: ignore
+ urllib.parse.urlparse(auth_redirect_uri.value).path # type: ignore
)
new_qr_redirect_uri = (
urllib.parse.urlparse(web_base_url).scheme # type: ignore
+ "://"
+ urllib.parse.urlparse(web_base_url).netloc # type: ignore
+ urllib.parse.urlparse(qr_redirect_uri.value).path # type: ignore
)
# 设置应用参数中的回调链接地址
auth_redirect_uri.write({"value": new_auth_redirect_uri}) # type: ignore
qr_redirect_uri.write({"value": new_qr_redirect_uri}) # type: ignore
auth_endpoint = "https://open.weixin.qq.com/connect/oauth2/authorize"
qr_auth_endpoint = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect"
try:
providers = (
self.env["auth.oauth.provider"]
.sudo()
.search(
[
"|",
("enabled", "=", True),
("enabled", "=", False),
]
)
)
except Exception:
providers = []
for provider in providers:
if auth_endpoint in provider["auth_endpoint"]:
provider.write(
{
# "client_id": client_id,
"validation_endpoint": auth_redirect_uri,
"enabled": True,
}
)
if qr_auth_endpoint in provider["auth_endpoint"]:
provider.write(
{
# "client_id": client_id,
"validation_endpoint": qr_redirect_uri,
"enabled": True,
}
)
else:
raise UserError(_("Please click on the 'Generate Parameters' button!"))
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -1,21 +0,0 @@
/** @odoo-module **//** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
// import * as ww from '@wecom/jssdk'
publicWidget.registry.WecomJion = publicWidget.Widget.extend({
selector: 'form.oe_login_form',
init() {
this._super(...arguments);
this.rpc = this.bindService("rpc");
},
start: function () {
this.companies = this.rpc({
route: "/wecom_login_info",
params: {
},
});
},
});
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
</templates>
@@ -1,20 +0,0 @@
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
// import * as ww from '@wecom/jssdk'
publicWidget.registry.WecomLogin = publicWidget.Widget.extend({
selector: '.o_login_auth',
events: {
'click a': '_onClick',
},
jsLibs: [
document.location.protocol + "//wwcdn.weixin.qq.com/node/open/js/wecom-jssdk-1.3.1.js",
],
_onClick: async function (ev) {
ev.preventDefault(); //阻止默认行为
let auth_url = $(ev.currentTarget).attr('href');
console.log(auth_url);
},
});
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
</templates>
@@ -1,33 +0,0 @@
<?xml version="1.0"?>
<odoo>
<record id="view_oauth_provider_tree" model="ir.ui.view">
<field name="name">auth.oauth.provider.tree</field>
<field name="model">auth.oauth.provider</field>
<field name="inherit_id" ref="auth_oauth.view_oauth_provider_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[last()]" position="after">
<field name="is_woecom" />
</xpath>
</field>
</record>
<record id="view_oauth_provider_form" model="ir.ui.view">
<field name="name">auth.oauth.provider.form</field>
<field name="model">auth.oauth.provider</field>
<field name="inherit_id" ref="auth_oauth.view_oauth_provider_form"/>
<field name="arch" type="xml">
<xpath expr="//group[last()]" position="after">
<group>
<field name="is_woecom" />
<field name="description" />
</group>
</xpath>
</field>
</record>
</odoo>
@@ -1,130 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">Settings</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="20"/>
<field name="inherit_id" ref="wecom_contacts.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="wecom_auth_oauth_warning" position="replace"/>
<div id="wecom_auth_oauth_install" position="after">
<div class="col-xs-12 col-md-6 o_setting_box" invisible="not is_wecom_organization">
<div class="o_setting_left_pane">
<i class="fa fa-puzzle-piece fa-lg" aria-label="Authentication Application"/>
</div>
<div class="o_setting_right_pane">
<label for="auth_app_id" string="Binding Auth Application"/>
<div class="content-group">
<div class="mt8">
<field name="auth_app_id" class="w-100" widget="selection" domain="['&amp;',('type_code', 'like', '%auth%'),('company_id', '=', company_id)]"/>
</div>
<button name="%(wecom_base.action_view_wecom_apps_list)d" icon="fa-arrow-right" type="action" string="Management application" class="btn-link" invisible="not auth_app_id"/>
<button class="btn btn-primary" name="get_app_info" string="Get application info" type="object" icon="fa-cloud-download" invisible="not auth_app_id" context="{'app': 'auth'}"/>
<div class="mt4" invisible="not auth_app_id">
<div class="text-warning mt8">
<strong>Save</strong> this page and come back here to set up other feature.</div>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" invisible="not auth_app_id">
<div class="o_setting_left_pane">
<i class="fa fa-list-ol fa-lg" aria-label="Auth AgentId"/>
</div>
<div class="o_setting_right_pane">
<label for="auth_agentid"/>
<div class="row">
<div class="text-muted col-md-12">
Application Management→Application→Open the application that needs to be verified and authorized, copy 'AgentId'
</div>
</div>
<div class="content-group">
<div class="mt8">
<field name="auth_agentid" widget="integer" options="{'format': false}" />
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" invisible="not auth_app_id">
<div class="o_setting_left_pane">
<i class="fa fa-key fa-lg" aria-label="Auth Secret"/>
</div>
<div class="o_setting_right_pane">
<label for="auth_secret"/>
<div class="row">
<div class="text-muted col-md-12">
Application Management→Application→Open the application that needs to be verified and authorized, copy 'Secret'
</div>
</div>
<div class="content-group">
<div class="mt8">
<field name="auth_secret" widget="password_char"/>
</div>
</div>
</div>
</div>
<!-- 身份认证应用的JSAPI临时凭证 -->
<div class="col-12 col-lg-12 o_setting_box" id="auth_app_jsapi_ticket_container" invisible="not auth_app_id">
<div class="o_setting_left_pane">
<i class="fa fa-ticket fa-lg" role="img" aria-label="JSAPI Ticket"></i>
</div>
<div class="o_setting_right_pane">
<label for="auth_app_jsapi_ticket" />
<div class="text-muted">
Is a temporary ticket for the H5 application to call the Wecom JS interface. Under normal circumstances, <b>the validity period of jsapi_ticket is 7200 seconds</b>, obtained by access_token. Since the number of API calls to the jsapi_ticket is very limited (an enterprise can get up to 400 times in an hour, and a single application cannot get more than 100 times), frequent refresh jsapi_ticket will cause the API call to be limited, affecting their own business, <b>and developers must cache jsapi_ticket in their own service global cache</b>.
</div>
<div class="content-group">
<div class="mt8">
<field name="auth_app_jsapi_ticket" readonly="1" force_save="1" widget="CopyClipboardText" class="w-100"/>
</div>
</div>
<div class="content-group">
<div class="mt8 d-flex justify-content-between">
<label for="auth_app_jsapi_ticket_expiration_time" string="Expiration time"/>
<field name="auth_app_jsapi_ticket_expiration_time" readonly="1" force_save="1" />
</div>
</div>
<div class="content-group mt8">
<button class="oe_highlight" name="get_auth_app_jsapi_ticket" type="object" string="Get JSAPI Ticket" icon="fa-ticket" />
</div>
</div>
</div>
<div class="col-xs-12 col-md-12 o_setting_box" invisible="not auth_app_id">
<div class="o_setting_left_pane">
<i class="fa fa-cogs fa-lg" aria-label="Authentication Configuration"/>
</div>
<div class="o_setting_right_pane">
<!-- <label for="auth_app_config_ids"/> -->
<div class="row">
<div class="content-group">
<!-- <div class="mt4">
<field name="auth_app_config_ids" widget="one2many" class="w-100" t-translation="off">
<tree create="0" edit="true" delete="0" editable="bottom">
<field name="name" readonly="1"/>
<field name="key" readonly="1"/>
<field name="value" required="1"/>
</tree>
</field>
</div> -->
<div class="btn-group float-right" role="group" aria-label="Button group">
<!-- <button class="btn btn-outline-primary" name="generate_parameters" context="{'code': 'auth'}" string="Generate parameters" type="object" icon="fa-cogs" /> -->
<button class="btn btn-outline-primary" name="enable_wecom_quick_login" type="object" string="Enable WeCom quick login" icon="fa-sign-in"/>
<button class="btn btn-outline-primary" type="action" name="%(auth_oauth.action_oauth_provider)d" string="View OAuth providers" icon="fa-arrow-right"/>
</div>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
</odoo>
-10
View File
@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
from . import models
def post_init_hook(env):
# env["ir.http"].clear_caches()
# env["ir.ui.view"].clear_caches()
env["ir.http"]._invalidate_cache()
env["ir.ui.view"]._invalidate_cache()
-49
View File
@@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
{
"name": "WeCom Base",
"author": "RStudio",
"sequence": 601,
"installable": True,
"application": True,
"auto_install": False,
"category": "WeCom Suites/Settings",
"website": "https://gitee.com/rainbowstudio/wecom",
"version": "17.0.0.1",
"summary": """
""",
"description": """
""",
"depends": ["base_setup", "wecom_widget", "wecom_api"],
"data": [
"security/wecom_security.xml",
"security/ir.model.access.csv",
"data/ir_module_category_data.xml",
"data/ir_config_parameter.xml",
"data/ir_cron_data.xml",
"data/wecom_app_type_data.xml",
"views/ir_cron_views.xml",
"views/res_config_settings_views.xml",
"views/res_company_views.xml",
"views/wecom_service_api_list_views.xml",
"views/wecom_service_api_error_views.xml",
"views/wecom_apps_views.xml",
"views/wecom_app_config_views.xml",
"views/wecom_app_callback_service_views.xml",
"views/wecom_app_event_type_views.xml",
"views/wecom_app_type_views.xml",
"views/wecom_app_subtype_views.xml",
"views/wecom_base_views.xml",
"views/menu_views.xml",
],
"assets": {
"web.assets_backend": [
"wecom_base/static/src/views/**/*",
],
},
"license": "AGPL-3",
"bootstrap": True,
"post_init_hook": "post_init_hook",
}
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
</data>
<data noupdate="1">
<function model="ir.config_parameter" name="set_param" eval="('wecom.debug_enabled', 'True')"/>
</data>
</odoo>
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record forcecreate="True" id="ir_cron_pull_wecom_service_api_error" model="ir.cron">
<field name="name">WeCom: Get the global error code of WeCom.</field>
<field name="model_id" ref="model_wecom_service_api_error"/>
<field name="state">code</field>
<field name="code">model.cron_pull_global_error_code()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">7</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active" eval="True"/>
<field name="doall" eval="False"/>
</record>
<record forcecreate="True" id="ir_cron_get_app_token" model="ir.cron">
<field name="name">WeCom: Get app token</field>
<field name="model_id" ref="model_wecom_apps"/>
<field name="state">code</field>
<field name="code">model.cron_get_app_token()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">2</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="active" eval="True"/>
<field name="doall" eval="False"/>
</record>
</data>
</odoo>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record model="ir.module.category" id="module_category_wecom_settings">
<field name="name">WeCom Settings</field>
<field name="sequence">1</field>
<field name="visible" eval="1" />
</record>
</data>
</odoo>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="base.main_company" model="res.company">
<field name="short_name">My Company</field>
</record>
</data>
</odoo>
-186
View File
@@ -1,186 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- 类型 -->
<!-- 类型:管理工具 -->
<record id="wecom_app_type_manage" model="wecom.app.type">
<field name="name">Manage Tools</field>
<field name="code">manage</field>
<field name="sequence">1</field>
</record>
<!-- 类型:基础应用 -->
<record id="wecom_app_type_base" model="wecom.app.type">
<field name="name">Base application</field>
<field name="code">base</field>
<field name="sequence">2</field>
</record>
<!-- 类型:外部联系-->
<record id="wecom_app_type_external_contact" model="wecom.app.type">
<field name="name">External contact</field>
<field name="code">external_contact</field>
<field name="sequence">3</field>
</record>
<!-- 类型:自建应用 -->
<record id="wecom_app_type_self" model="wecom.app.type">
<field name="name">Self built application</field>
<field name="code">self</field>
<field name="sequence">4</field>
</record>
<!-- 类型:第三方应用 -->
<record id="wecom_app_type_third" model="wecom.app.type">
<field name="name">Third party application</field>
<field name="code">third</field>
<field name="sequence">5</field>
</record>
<!-- 子类型 -->
<!-- 子类型 :通讯录管理-->
<record id="wecom_app_subtype_contacts_management" model="wecom.app.subtype">
<field name="name">Contacts management</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_manage"/>
<field name="code">contacts</field>
<field name="sequence">1</field>
</record>
<!-- 子类型 :客户联系 -->
<record id="wecom_app_subtype_customer_contact" model="wecom.app.subtype">
<field name="name">Customer contact</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_external_contact"/>
<field name="code">customer</field>
<field name="sequence">2</field>
</record>
<!-- 子类型 :微信客服 -->
<record id="wecom_app_subtype_kf" model="wecom.app.subtype">
<field name="name">Wechat customer service</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">kf</field>
<field name="sequence">3</field>
</record>
<!-- 子类型 : 身份验证 -->
<record id="wecom_app_subtype_auth" model="wecom.app.subtype">
<field name="name">Authentication</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_self"/>
<field name="code">auth</field>
<field name="sequence">4</field>
</record>
<!-- 子类型 : 应用管理 -->
<record id="wecom_app_subtype_app" model="wecom.app.subtype">
<field name="name">Application management</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_self"/>
<field name="code">app</field>
<field name="sequence">5</field>
</record>
<!-- 子类型 : 消息推送 -->
<record id="wecom_app_subtype_message_push" model="wecom.app.subtype">
<field name="name">Message push</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_self"/>
<field name="code">message</field>
<field name="sequence">6</field>
</record>
<!-- 子类型 : 素材管理 -->
<record id="wecom_app_subtype_material_management" model="wecom.app.subtype">
<field name="name">Material management</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_self"/>
<field name="code">material</field>
<field name="sequence">7</field>
</record>
<!-- 子类型 : OA-打卡 -->
<record id="wecom_app_subtype_oa_checkin" model="wecom.app.subtype">
<field name="name">OA:Checkin</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">checkin</field>
<field name="sequence">8</field>
</record>
<!-- 子类型 : OA-审批 -->
<record id="wecom_app_subtype_oa_approval" model="wecom.app.subtype">
<field name="name">OA:Approval</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">approval</field>
<field name="sequence">9</field>
</record>
<!-- 子类型 : OA-汇报 -->
<record id="wecom_app_subtype_oa_journal" model="wecom.app.subtype">
<field name="name">OA:Journal</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">journal</field>
<field name="sequence">10</field>
</record>
<!-- 子类型 : OA-会议室 -->
<record id="wecom_app_subtype_oa_meetingroom" model="wecom.app.subtype">
<field name="name">OA:Meeting room</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">meetingroom</field>
<field name="sequence">11</field>
</record>
<!-- 子类型 : 效率工具-企业邮箱 -->
<record id="wecom_app_subtype_efficiency_tools_exmail" model="wecom.app.subtype">
<field name="name">Efficiency tools:Corporate email</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">exmail</field>
<field name="sequence">12</field>
</record>
<!-- 子类型 : 效率工具-日程 -->
<record id="wecom_app_subtype_efficiency_tools_calendar" model="wecom.app.subtype">
<field name="name">Efficiency tools:Calendar</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">calendar</field>
<field name="sequence">13</field>
</record>
<!-- 子类型 : 效率工具-直播 -->
<record id="wecom_app_subtype_efficiency_tools_living" model="wecom.app.subtype">
<field name="name">Efficiency tools:Living</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">living</field>
<field name="sequence">14</field>
</record>
<!-- 子类型 : 效率工具-微盘 -->
<record id="wecom_app_subtype_efficiency_tools_wedrive" model="wecom.app.subtype">
<field name="name">Efficiency tools:We Drive</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">wedrive</field>
<field name="sequence">15</field>
</record>
<!-- 子类型 : 效率工具-公费电话 -->
<record id="wecom_app_subtype_efficiency_tools_dial" model="wecom.app.subtype">
<field name="name">Efficiency tools:Public telephone</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_base"/>
<field name="code">dial</field>
<field name="sequence">16</field>
</record>
<!-- 子类型 : 会话内容存档 -->
<record id="wecom_app_subtype_msgaudit" model="wecom.app.subtype">
<field name="name">Session content archiving</field>
<field name="parent_id" ref="wecom_base.wecom_app_type_manage"/>
<field name="code">msgaudit</field>
<field name="sequence">17</field>
</record>
</data>
</odoo>
File diff suppressed because it is too large Load Diff
-19
View File
@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from . import ir_http
from . import wecom_server_api
from . import wecom_server_api_error
from . import wecom_server_api_list
from . import ir_module
from . import res_company
from . import res_config_settings
from . import res_partner
from . import wecom_app_type
from . import wecom_app_subtype
from . import wecom_apps
from . import wecom_app_callback_service
from . import wecom_app_config
from . import wecom_app_event_type
-25
View File
@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models
from odoo.http import request
class Http(models.AbstractModel):
_inherit = 'ir.http'
def session_info(self):
session_info = super(Http, self).session_info() # type: ignore
user = request.env.user # type: ignore
if self.env.user.has_group('base.group_user'):
allowed_companies = session_info["user_companies"]["allowed_companies"]
allowed_companies = {
comp.id: {
'id': comp.id,
'name': comp.name,
'is_wecom_organization': comp.is_wecom_organization,
'sequence': comp.sequence,
} for comp in user.company_ids
}
session_info["user_companies"]["allowed_companies"] = allowed_companies
return session_info
-15
View File
@@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
class Module(models.Model):
_inherit = "ir.module.module"
def wecom_check_module_installed(self, modules_name):
# modules 输入参数是个 list,如 ['base', 'sale']
module = self.sudo().search([("name", "=", modules_name)])
if not module or module.state != "installed": # type: ignore
return False
else:
return True
-23
View File
@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
import io
import logging
import os
from odoo import api, fields, models, tools, _
class Company(models.Model):
_inherit = "res.company"
# 基础
short_name = fields.Char("Short Name", translate=True)
is_wecom_organization = fields.Boolean("WeCom organization", default=False)
corpid = fields.Char("Corp ID")
# 企业的jsapi_ticket
corp_jsapi_ticket = fields.Char(string="Corp JSAPI Ticket", copy=False)
corp_jsapi_ticket_expiration_time = fields.Datetime(
string="Expiration time of Corp JSAPI ticket", copy=False
)
-167
View File
@@ -1,167 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from random import weibullvariate
from odoo import models, fields, api, _
from odoo.addons.wecom_api.api.wecom_abstract_api import ApiException # type: ignore
import logging
_logger = logging.getLogger(__name__)
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
# 基础
is_wecom_organization = fields.Boolean(
related="company_id.is_wecom_organization", readonly=False
)
company_name = fields.Char(
related="company_id.name", string="Company Name", readonly=False
)
short_name = fields.Char(related="company_id.short_name", readonly=False)
corpid = fields.Char(string="Corp ID", related="company_id.corpid", readonly=False)
corp_jsapi_ticket = fields.Char(
related="company_id.corp_jsapi_ticket",
readonly=False,
)
corp_jsapi_ticket_expiration_time = fields.Datetime(
related="company_id.corp_jsapi_ticket_expiration_time",
readonly=False,
)
debug_enabled = fields.Boolean("Turn on debug mode", default=True)
resources_path = fields.Char(
"WeCom resources storage path",
config_parameter="wecom.resources_path",
)
global_error_code_url = fields.Char(
"Global error code page URL",
config_parameter="wecom.global_error_code_url",
)
global_error_code_item_selection_code = fields.Char(
"Global error code item selection code",
config_parameter="wecom.global_error_code_item_selection_code",
) # 错误码排查方法的选取属性。
module_web_theme = fields.Boolean("Rainbow Theme")
module_wecom_contacts = fields.Boolean("WeCom Contacts")
module_wecom_contacts_sync = fields.Boolean("WeCom Contacts Synchronized")
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values() # type: ignore
ir_config = self.env["ir.config_parameter"].sudo()
debug_enabled = (
True if ir_config.get_param("wecom.debug_enabled") == "True" else False
)
res.update(
debug_enabled=debug_enabled,
)
return res
def set_values(self):
super(ResConfigSettings, self).set_values() # type: ignore
ir_config = self.env["ir.config_parameter"].sudo()
ir_config.set_param("wecom.debug_enabled", self.debug_enabled or "False")
def open_wecom_company(self):
return {
"type": "ir.actions.act_window",
"name": "My Company",
"view_mode": "form",
"res_model": "res.company",
"res_id": self.env.company.id,
"target": "current",
"context": {
"form_view_initial_mode": "edit",
},
}
@api.depends("company_id")
def _compute_wecom_company_corpid(self):
company_corpid = self.company_id.corpid if self.company_id.corpid else "" # type: ignore
for record in self:
record.wecom_company_corpid = company_corpid # type: ignore
@api.model
def open_wecom_settings(self):
action = self.env["ir.actions.actions"]._for_xml_id(
"wecom_base.res_config_settings_view_form"
)
action["target"] = "new"
return action
def get_app_info(self):
"""
获取企业应用信息
:param agentid:
:return:
"""
def get_corp_jsapi_ticket(self):
"""
获取企业的jsapi_ticket
:return:
"""
ir_config = self.env["ir.config_parameter"].sudo()
debug = ir_config.get_param("wecom.debug_enabled")
if debug:
_logger.info(
_("Start getting ticket for company [%s]") % (self.company_id.name) # type: ignore
)
try:
if (
self.corp_jsapi_ticket_expiration_time
and self.corp_jsapi_ticket_expiration_time > datetime.now()
):
# 未过期,
# print("未过期")
msg = {
"title": _("Tips"),
"message": _("Ticket is still valid, and no update is required!"),
"sticky": False,
}
return self.env["wecomapi.tools.action"].WecomInfoNotification(msg)
else:
# print("过期")
wecom_api = self.env["wecom.service_api"].InitServiceApi(self.company_id.corpid, self.contacts_app_id.secret) # type: ignore
response = wecom_api.httpCall(
self.env["wecom.service_api_list"].get_server_api_call(
"GET_JSAPI_TICKET"
),
{},
)
# print(response)
except ApiException as ex:
return self.env["wecomapi.tools.action"].ApiExceptionDialog(
ex, raise_exception=True
)
else:
if response["errcode"] == 0:
self.write(
{
"corp_jsapi_ticket": response["ticket"],
"corp_jsapi_ticket_expiration_time": datetime.now()
+ timedelta(seconds=response["expires_in"]),
}
)
msg = {
"title": _("Tips"),
"message": _("Successfully obtained ticket!"),
"sticky": False,
"next": {
"type": "ir.actions.client",
"tag": "reload",
},
}
return self.env["wecomapi.tools.action"].WecomSuccessNotification(msg)
-7
View File
@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, _
class Partner(models.Model):
_inherit = "res.partner"
@@ -1,103 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class WeComAppCallbackService(models.Model):
"""
接收事件服务器配置
https://work.weixin.qq.com/api/doc/90000/90135/90930
"""
_name = "wecom.app_callback_service"
_description = "Wecom Application receive event service"
app_id = fields.Many2one(
"wecom.apps",
string="Application",
copy=False,
ondelete="cascade",
default=lambda self: self.env["wecom.apps"].id,
# domain="[('company_id', '=', company_id)]",
required=True,
)
name = fields.Char(string="Service Name", required=True, translate=True)
code = fields.Char(
string="Service Code",
copy=False,
required=True,
)
callback_url = fields.Char(
string="URL",
store=True,
readonly=True,
compute="_default_callback_url",
copy=False,
) # 回调服务地址
callback_url_token = fields.Char(string="Token", copy=False) # Token用于计算签名
callback_aeskey = fields.Char(string="AES Key", copy=False) # 用于消息内容加密
description = fields.Text(string="Description", translate=True, copy=True)
active = fields.Boolean("Active", default=False)
_sql_constraints = [
(
"app_code_uniq",
"unique (app_id,code)",
_("The code of each application must be unique !"),
)
]
@api.depends("app_id", "code")
def _default_callback_url(self):
"""
默认回调地址
:return:"""
params = self.env["ir.config_parameter"].sudo()
base_url = params.get_param("web.base.url")
callback_url = ""
for server in self:
if server.app_id.company_id and server.code: # type: ignore
callback_url = base_url + "/wecom_callback/%s/%s" % ( # type: ignore
server.app_id.company_id.id, # type: ignore
server.code, # type: ignore
)
server.callback_url = callback_url # type: ignore
@api.onchange("app_id", "code")
def _onchange_callback_url(self):
"""
当应用发生变化时,更新回调服务地址
:return:
"""
params = self.env["ir.config_parameter"].sudo()
base_url = params.get_param("web.base.url")
if self.app_id:
self.callback_url = base_url + "/wecom_callback/%s/%s" % (
self.app_id.company_id.id, # type: ignore
self.code,
)
else:
self.callback_url = ""
def generate_contact_service(self):
"""
生成通讯录回调服务
:return:
"""
params = self.env["ir.config_parameter"].sudo()
base_url = params.get_param("web.base.url")
if not self.app_id:
raise ValidationError(_("Please bind contact app!"))
else:
callback_url = base_url + "/wecom_callback/%s/%s" % (
self.app_id.company_id.id, # type: ignore
self.code,
)
return callback_url
-161
View File
@@ -1,161 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, api, _
from odoo.tools import config, ormcache, mute_logger
FIELD_TYPES = [(key, key) for key in sorted(fields.Field.by_type)]
class WeComAppConfig(models.Model):
_name = "wecom.app_config"
_description = "Wecom Application Configuration"
_table = "wecom_app_config"
# _rec_name = "key"
_order = "id"
app_id = fields.Many2one( # type: ignore
"wecom.apps",
string="Application",
copy=False,
ondelete="cascade",
default=lambda self: self.env["wecom.apps"].id,
# domain="[('company_id', '=', company_id)]",
required=True,
)
company_id = fields.Many2one(related="app_id.company_id", store=True)
name = fields.Char(string="Name", translate=True, required=True, copy=True)
key = fields.Char(
required=True,
)
ttype = fields.Selection(
selection=FIELD_TYPES, string="Field Type", required=True, copy=True
)
value = fields.Text(required=True)
description = fields.Html(string="Description", translate=True, copy=True)
_sql_constraints = [
(
"app_id_key_uniq",
"unique (app_id,key)",
_("The key of each application must be unique !"),
)
]
def get_format_field_value_and_type(self, res_id=False, field_name=""):
"""
JS 获取需要格式化的字段的类型和值
:return:
"""
res = self.browse(res_id)
return res.ttype # type: ignore
# return {
# "id": res_id,
# "value_type": res.ttype,
# "value": res.value,
# }
def update_config(self, res_id=False, value=""):
"""
更新参数
"""
# res = self.browse(res_id)
app_config = (
self.env["wecom.app_config"]
.sudo()
.search(
[
("id", "=", res_id),
]
)
)
app_config.sudo().write({"value": value})
print(app_config.value)
return app_config.value
@api.model
def get_param(self, app_id, key, default=False):
"""检索给定key的value
:param string key: 要检索的参数值的键。
:param string default: 如果缺少参数,则为默认值。
:return: 参数的值, 如果不存在,则为 ``default``.
:rtype: string
"""
return self._get_param(app_id, key) or default
def _get_param(self, app_id, key):
params = self.search_read(
[("app_id", "=", app_id), ("key", "=", key)],
fields=["ttype", "value"],
limit=1,
)
value = None
ttype = None
if not params:
# 如果没有找到,搜索其他公司相同应用配置参数 进行复制
copy_app_config = self.search([("key", "=", key)], limit=1)
if copy_app_config:
new_app_config = self.create(
{
"app_id": app_id,
"name": copy_app_config.name, # type: ignore
"key": copy_app_config.key, # type: ignore
"ttype": copy_app_config.ttype, # type: ignore
"value": copy_app_config.value, # type: ignore
"description": copy_app_config.description, # type: ignore
}
)
value = new_app_config.value
ttype = new_app_config.ttype
else:
pass
else:
value = params[0]["value"]
ttype = params[0]["ttype"]
if ttype == "boolean":
boolean_value = str(value).lower()
if boolean_value in ["true", "yes", "t", "1"]:
return True
elif boolean_value in ["false", "no", "f", "0"]:
return False
else:
return False
return value if params else None
@api.model
def set_param(self, app_id, key, value):
"""设置参数的值。
:param string key: 要设置的参数值的键。
:param string value: 要设置的值。
:return: 参数的上一个值,如果不存在,则为False。
:rtype: string
"""
param = self.search([("app_id", "=", app_id), ("key", "=", key)])
if param:
old = param.value # type: ignore
if value is not False and value is not None:
if str(value) != old:
param.write({"value": value}) # type: ignore
else:
param.unlink() # type: ignore
return old
else:
if value is not False and value is not None:
self.create({"app_id": app_id, "key": key, "value": value})
return False
@api.model_create_multi
def create(self, vals_list):
self._invalidate_cache()
return super(WeComAppConfig, self).create(vals_list)
def write(self, vals):
self._invalidate_cache()
return super(WeComAppConfig, self).write(vals)
def unlink(self):
self._invalidate_cache()
return super(WeComAppConfig, self).unlink()
-152
View File
@@ -1,152 +0,0 @@
# -*- coding: utf-8 -*-
import logging
import base64
from odoo import _, api, fields, models
from odoo.exceptions import MissingError, UserError, ValidationError, AccessError
from odoo.tools.safe_eval import safe_eval, test_python_expr
from lxml import etree
from lxml import etree as ET
from odoo.http import Response
# from lxml_to_dict import lxml_to_dict
# from xmltodict import lxml_to_dict
import xmltodict, json
_logger = logging.getLogger(__name__)
class WeComAppEventType(models.Model):
_name = "wecom.app.event_type"
_description = "Wecom Application Event"
_order = "id"
DEFAULT_PYTHON_CODE = """# Available variables:
# - env: Odoo Environment on which the action is triggered
# - model: Odoo Model of the record on which the action is triggered; is a void recordset
# - record: record on which the action is triggered; may be void
# - records: recordset of all records on which the action is triggered in multi-mode; may be void
# - time, datetime, dateutil, timezone: useful Python libraries
# - float_compare: Odoo function to compare floats based on specific precisions
# - log: log(message, level='info'): logging function to record debug information in ir.logging table
# - UserError: Warning Exception to use with raise
# To return an action, assign: action = {...}\n\n\n\n"""
name = fields.Char(
string="Name",
translate=True,
copy=False,
required=True,
)
model_ids = fields.Many2many(
"ir.model",
string="Related Model",
)
msg_type = fields.Char(
string="Message Type", copy=False, required=True, default="event"
)
event = fields.Char(
string="Event Code",
copy=False,
required=True,
)
change_type = fields.Char(string="Change Type", copy=False)
code = fields.Char(
string="Python Code",
default="",
)
command = fields.Char(string="Command", copy=False)
def handle_event(self):
"""
处理事件
"""
xml_tree = self.env.context.get("xml_tree")
company_id = self.env.context.get("company_id")
event_str = etree.fromstring(xml_tree).find("Event").text # type: ignore
changetype_str = etree.fromstring(xml_tree).find("ChangeType").text # type: ignore
_logger.info(
_(
"Received the callback notification of Wecom, event [%s], change type [%s]."
)
% (
event_str,
changetype_str,
)
)
event = (
self.env["wecom.app.event_type"]
.sudo()
.search(
[
("event", "=", event_str),
("change_type", "=", changetype_str),
]
)
)
if not event:
_logger.warning(
_(
"Cannot find [%s] change type for executing company [%s], ignoring it."
)
% (
event.name,
company_id.name,
)
)
return
if event.code:
# ^ 正确响应企业微信本次的POST请求,企业微信将不会再次发送请求
# ^ ·企业微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次
# ^ ·当接收成功后,http头部返回200表示接收ok,其他错误码企业微信后台会一律当做失败并发起重试
try:
event.with_context(
xml_tree=xml_tree, company_id=company_id
).sudo().run()
except Exception as e:
_logger.warning(
_(
"Unable to execute [%s] change type for company [%s], ignoring it. reason: %s"
)
% (event.name, company_id.name, str(e))
)
return Response("success", status=200)
else:
return Response("success", status=200)
def run(self):
"""
执行事件
"""
xml_tree = self.env.context.get("xml_tree")
company_id = self.env.context.get("company_id")
func_name = self.code.split("model.")[1]
cmd = self.command
for m in self.model_ids:
model_obj = self.env.get(m.model) # type: ignore
if hasattr(model_obj, func_name):
# 存在函数
func = getattr(
model_obj.with_context(xml_tree=xml_tree, company_id=company_id),
func_name,
)
func(cmd)
_logger.info(
_("Method [%s] to execute model [%s]")
% (
func_name,
model_obj,
)
)
else:
_logger.warning(
_("Function [%s] does not exist in model [%s]")
% (
func_name,
model_obj,
)
)
-24
View File
@@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import _, api, fields, models
class WeComAppType(models.Model):
_name = "wecom.app.subtype"
_description = "Wecom Application Subtype"
_order = "parent_id,sequence"
name = fields.Char(string="Name", translate=True, copy=False, required=True,)
parent_id = fields.Many2one(
"wecom.app.type",
ondelete="cascade",
string="Parent",
index=True,
copy=False,
required=True,
)
code = fields.Char(string="Code", copy=False, required=True,)
sequence = fields.Integer(default=0, copy=True)
_sql_constraints = [("code_uniq", "unique (code)", _("Code must be unique !"),)]
-35
View File
@@ -1,35 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import _, api, fields, models
class WeComAppType(models.Model):
_name = "wecom.app.type"
_description = "Wecom Application Type"
_order = "sequence"
name = fields.Char(
string="Name",
translate=True,
copy=False,
required=True,
)
code = fields.Char(
string="Code",
copy=False,
required=True,
)
subtype_ids = fields.One2many(
"wecom.app.subtype",
"parent_id",
string="Application Subtype",
)
sequence = fields.Integer(default=0, copy=True)
_sql_constraints = [
(
"code_uniq",
"unique (code)",
_("Code must be unique !"),
)
]
-432
View File
@@ -1,432 +0,0 @@
# -*- coding: utf-8 -*-
import logging
import time
from datetime import datetime, timedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.addons.wecom_api.api.wecom_abstract_api import ApiException # type: ignore
_logger = logging.getLogger(__name__)
class WeComApps(models.Model):
_name = "wecom.apps"
_description = "Wecom Application"
_order = "sequence"
name = fields.Char(
string="Name",
copy=False,
compute="_compute_name",
store=True,
index=True,
) # 企业应用名称
app_name = fields.Char(
string="Application Name",
translate=True,
copy=True,
) # 应用名称
company_id = fields.Many2one(
"res.company",
string="Company",
domain="[('is_wecom_organization', '=', True)]",
copy=False,
store=True,
)
# model_ids = fields.Many2many("ir.model", string="Related Model",)
# 应用类型 required=True
type = fields.Selection(
selection=lambda self: self._type_selection_values(),
string="Application Type",
required=True,
copy=True,
default="manage",
)
type_id = fields.Many2one("wecom.app.type", string="Application Types", store=True)
subtype_ids = fields.Many2many(
"wecom.app.subtype",
string="Application Subtype",
)
type_code = fields.Char(
string="Application type code",
store=True,
readonly=True,
compute="_computet_type_code",
)
def write(self, vals):
for app in self:
labels = dict(self.fields_get(allfields=["type"])["type"]["selection"])[app.type] # type: ignore
if "company_id" in vals:
company = app.company_id # type: ignore
vals["name"] = "%s/%s/%s" % (
company.short_name,
labels,
app.app_name, # type: ignore
)
else:
vals["name"] = "%s/%s" % (labels, app.app_name) # type: ignore
result = super(WeComApps, self).write(vals)
return result
@api.depends("subtype_ids")
def _computet_type_code(self):
"""
计算应用类型代码
"""
if self.subtype_ids:
self.type_code = str(self.subtype_ids.mapped("code")) # type: ignore
else:
self.type_code = "[]"
def get_type_code(self):
"""
测试代码
"""
type_code = str(self.subtype_ids.mapped("code")) # type: ignore
print(type_code, type(type_code))
@api.onchange("subtype_ids")
def _onchange_subtype_ids(self):
"""
变更子类型
:return:
"""
if self.type_id.code == "manage" or self.type_id.code == "base": # type: ignore
if len(self.subtype_ids) > 1:
raise UserError(
_("Only one subtype can be selected for the current app type!")
)
@api.model
def _type_selection_values(self):
models = self.env["wecom.app.type"].sudo().search([]).sorted("sequence")
return [(model.code, model.name) for model in models]
@api.onchange("type")
def _onchange_type(self):
# self.subtype_ids = False
# self.type_code = ""
# if self.type:
# type = self.env["wecom.app.type"].sudo().search([("code", "=", self.type)])
# self.type_id = type
# return {"domain": {"subtype_ids": [("parent_id", "=", type.id)]}}
# else:
# self.type_id = False
# return {"domain": {"subtype": []}}
if self.subtype_ids:
self.type_code = str(self.subtype_ids.mapped("code")) # type: ignore
else:
self.type_code = "[]"
agentid = fields.Integer(string="Agent ID", copy=False) # 企业应用id
secret = fields.Char("Secret", default="", copy=False)
square_logo_url = fields.Char(string="Square Logo", copy=True) # 企业应用方形头像
description = fields.Text(string="Description", translate=True, copy=True) # 企业应用详情
allow_userinfos = fields.Char(
string="Visible range (personnel)", copy=False
) # 企业应用可见范围(人员),其中包括userid
allow_partys = fields.Char(
string="Visible range (Department)", copy=False
) # 企业应用可见范围(部门)
allow_tags = fields.Char(string="Visible range (Tag))", copy=False) # 企业应用可见范围(标签)
close = fields.Boolean(string="Disabled", copy=False) # 企业应用是否被停用
redirect_domain = fields.Char(string="Trusted domain name", copy=True) # 企业应用可信域名
report_location_flag = fields.Boolean(
string="Open the geographic location and report", copy=False
) # 企业应用是否打开地理位置上报 0:不上报;1:进入会话上报;
isreportenter = fields.Boolean(
string="Report user entry event", copy=False
) # 企业应用是否打开进入会话上报 0:不上报;1:进入会话上报;
home_url = fields.Char(string="Home page", copy=True) # 企业应用主页url
sequence = fields.Integer(default=0, copy=True)
menu_body = fields.Text(
"Application menu data",
translate=True,
default="{}",
copy=False,
)
# 访问令牌
access_token = fields.Char(string="Access Token", readonly=True, copy=False)
token_expiration_time = fields.Datetime(
string="Token Expiration Time", readonly=True, copy=False
)
# 应用的jsapi_ticket
jsapi_ticket = fields.Char(string="JSAPI Ticket", readonly=True, copy=False)
jsapi_ticket_expiration_time = fields.Datetime(
string="JSAPI Ticket Expiration Time", readonly=True, copy=False
)
_sql_constraints = [
(
"company_id_key_uniq",
"unique (company_id,app_name)",
_("The application name of each company must be unique !"),
)
]
@api.model_create_multi
def create(self, values):
res = super(WeComApps, self).create(values)
if res.subtype_ids:
res.type_code = str(res.subtype_ids.mapped("code"))
else:
res.type_code = "[]"
return res
@api.depends("company_id", "app_name", "type")
def _compute_name(self):
for app in self:
labels = dict(self.fields_get(allfields=["type"])["type"]["selection"])[app.type] # type: ignore
if app.company_id: # type: ignore
app.name = "%s/%s/%s" % (app.company_id.short_name,labels,app.app_name,) # type: ignore
else:
app.name = "%s/%s" % (labels, app.app_name) # type: ignore
# 回调服务
app_callback_service_ids = fields.One2many(
"wecom.app_callback_service",
"app_id",
string="Receive event service",
domain="['|', ('active', '=', True), ('active', '=', False)]",
context={"active_test": False},
)
# 应用参数配置
app_config_ids = fields.One2many(
"wecom.app_config",
"app_id",
string="Application Configuration",
# context={
# "default_company_id": lambda self: self.company_id,
# },
) # 应用参数配置
# ————————————————————————————————————
# 应用回调服务
# ————————————————————————————————————
def generate_service(self):
"""
生成回调服务
:return:
"""
code = self.env.context.get("code") # 按钮的传递值
if bool(code):
# 存在按钮的传递值,通过按钮的传递值生成回调服务
self.generate_service_by_code(code)
else:
# 不存在按钮的传递值,通过子类型生成生成回调服务
self.generate_service_by_subtype()
def generate_service_by_subtype(self):
"""
通过子类型生成生成回调服务
"""
for record in self.subtype_ids:
self.generate_service_by_code(record.code) # type: ignore
def generate_service_by_code(self, code):
"""
根据code生成回调服务
:param code:
:return:
"""
# ————————————————————————————————————
# 应用参数配置
# ————————————————————————————————————
def generate_parameters(self):
"""
生成参数
:return:
"""
code = self.env.context.get("code") # 按钮的传递值
if bool(code):
# 存在按钮的传递值,通过按钮的传递值生成回调服务
self.generate_parameters_by_code(code)
else:
# 不存在按钮的传递值,通过子类型生成生成回调服务
self.generate_parameters_by_subtype()
def generate_parameters_by_subtype(self):
"""
通过子类型生成生成参数
"""
for record in self.subtype_ids:
self.generate_parameters_by_code(record.code) # type: ignore
def generate_parameters_by_code(self, code):
"""
根据code生成参数
:param code:
:retur
"""
# ————————————————————————————————————
# 应用信息
# ————————————————————————————————————
def get_app_info(self):
"""
获取企业应用信息
:param agentid:
:return:
"""
for record in self:
try:
wecomapi = self.env["wecom.service_api"].InitServiceApi(record.company_id.corpid, record.secret) # type: ignore
response = wecomapi.httpCall(
self.env["wecom.service_api_list"].get_server_api_call("AGENT_GET"),
{"agentid": str(record.agentid)}, # type: ignore
)
except ApiException as e:
return self.env["wecomapi.tools.action"].ApiExceptionDialog(
e, raise_exception=True
)
else:
if response["errcode"] == 0:
record.write(
{
"app_name": response["name"],
"square_logo_url": response["square_logo_url"],
"description": response["description"],
"allow_userinfos": response["allow_userinfos"]
if "allow_userinfos" in response
else "{}",
"allow_partys": response["allow_partys"]
if "allow_partys" in response
else "{}",
"allow_tags": response["allow_tags"]
if "allow_tags" in response
else "{}",
"close": response["close"],
"redirect_domain": response["redirect_domain"],
"report_location_flag": response["report_location_flag"],
"isreportenter": response["isreportenter"],
"home_url": response["home_url"],
}
)
# msg = {
# "title": _("Tips"),
# "message": _("Successfully obtained application information!"),
# "sticky": False,
# }
# return self.env["wecomapi.tools.action"].WecomSuccessNotification(msg)
def set_app_info(self):
"""
设置企业应用信息
:param agentid:
:return:
"""
# ————————————————————————————————————
# 应用令牌
# ————————————————————————————————————
def get_access_token(self):
"""获取企业应用接口调用凭据(令牌)
:return:
"""
ir_config = self.env["ir.config_parameter"].sudo()
debug = ir_config.get_param("wecom.debug_enabled")
if debug:
_logger.info(
_("Start getting app [%s] token for company [%s]")% (self.name, self.company_id.name) # type: ignore
)
try:
wecom_api = self.env["wecom.service_api"].InitServiceApi(self.company_id.corpid, self.secret) # type: ignore
except ApiException as ex:
return self.env["wecomapi.tools.action"].ApiExceptionDialog(
ex, raise_exception=True
)
# finally:
# if self.token_expiration_time and self.token_expiration_time > datetime.now():
# # 令牌未过期,则直接返回 提示信息
# msg = {
# "title": _("Tips"),
# "message": _("Token is still valid, and no update is required!"),
# "sticky": False,
# }
# return self.env["wecomapi.tools.action"].WecomInfoNotification(msg)
def cron_get_app_token(self):
"""
自动任务定时获取应用token
"""
for app in self.search([("company_id", "!=", False)]): # type: ignore
_logger.info(
_(
"Automatic task: start to get the application [%s] token of company [%s]"
)
% (app.name, app.company_id.name)
)
app.get_access_token()
# ————————————————————————————————————
# 应用 jsapi_ticket
# ————————————————————————————————————
def get_app_jsapi_ticket(self):
"""
获取应用的jsapi_ticket
"""
ir_config = self.env["ir.config_parameter"].sudo()
debug = ir_config.get_param("wecom.debug_enabled")
if debug:
_logger.info(
_("Start getting app [%s] ticket for company [%s]")
% (self.name, self.company_id.name) # type: ignore
)
try:
if (
self.jsapi_ticket_expiration_time
and self.jsapi_ticket_expiration_time > datetime.now()
):
# 未过期,
# print("未过期")
msg = {
"title": _("Tips"),
"message": _("Ticket is still valid, and no update is required!"),
"sticky": False,
}
return self.env["wecomapi.tools.action"].WecomInfoNotification(msg)
else:
# print("过期")
wecom_api = self.env["wecom.service_api"].InitServiceApi(
self.company_id.corpid, self.secret # type: ignore
)
response = wecom_api.httpCall(
self.env["wecom.service_api_list"].get_server_api_call(
"AGENT_GET_TICKET"
),
{"type": "agent_config"},
)
except ApiException as ex:
return self.env["wecomapi.tools.action"].ApiExceptionDialog(
ex, raise_exception=True
)
else:
if response["errcode"] == 0:
self.write(
{
"jsapi_ticket": response["ticket"],
"jsapi_ticket_expiration_time": datetime.now()
+ timedelta(seconds=response["expires_in"]),
}
)
msg = {
"title": _("Tips"),
"message": _("Successfully obtained application ticket!"),
"sticky": False,
"next": {
"type": "ir.actions.client",
"tag": "reload",
},
}
finally:
return self.env["wecomapi.tools.action"].WecomSuccessNotification(msg) # type: ignore
-7
View File
@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from odoo import api, fields, models, SUPERUSER_ID, _
class WecomServerApi(models.TransientModel):
_inherit = "wecom.service_api"
@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
import requests
import logging
from urllib.parse import quote, unquote
import pandas as pd
pd.set_option("max_colwidth", 4096)
from lxml import etree
import requests
from odoo import api, fields, models, SUPERUSER_ID, _
_logger = logging.getLogger(__name__)
class WecomServerApiError(models.Model):
_inherit = "wecom.service_api_error"

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