企业微信 通步

This commit is contained in:
pengyuthon
2024-07-01 20:41:30 +08:00
parent 9e92b1368e
commit 936f70e039
36 changed files with 2078 additions and 77 deletions
@@ -6,25 +6,15 @@
<field name="name">采购办公用品</field>
<field name="approval_type">purchase_work</field>
</record>
<!-- 天河投资办公室 - 党总支发文 -->
<record id="approval_category_data_tianhe_general_party_branch_post" model="approval.category">
<field name="name">党总支发文</field>
<field name="approval_type">general_party_branch_post</field>
</record>
<!-- 天河投资办公室 - 建设工程业务呈批表 -->
<record id="approval_category_data_tianhe_construction_business_approval" model="approval.category">
<field name="name">建设工程业务呈批表</field>
<field name="approval_type">construction_business</field>
</record>
<!-- 天河投资办公室 - 董事会议题申报表 -->
<record id="approval_category_data_tianhe_dsh_issues_declarations" model="approval.category">
<field name="name">董事会议题申报表</field>
<field name="approval_type">tianhe_dsh_issues_declarations</field>
</record>
<!-- 天河投资办公室 - 工资呈批表 -->
<record id="approval_category_data_tianhe_salary_approval" model="approval.category">
<field name="name">工资呈批表</field>
<field name="approval_type">tianhe_dsh_issues_declarations</field>
<field name="approval_type">salary_approval</field>
</record>
<!-- 天河投资办公室 - 经营班子申报表 -->
<record id="approval_category_data_tianhe_management_declare_approval" model="approval.category">
@@ -32,11 +22,6 @@
<field name="approval_type">management_declare</field>
<field name="sequence">1</field>
</record>
<!-- 天河投资办公室 - 公文发文(子企业) -->
<record id="approval_category_data_tianhe_official_issues_for_son" model="approval.category">
<field name="name">公文发文(子企业)</field>
<field name="approval_type">official_issues_for_son</field>
</record>
<!-- 天河投资办公室 - 公文发文 -->
<record id="approval_category_data_tianhe_official_issues" model="approval.category">
<field name="name">公文发文</field>
@@ -67,11 +52,6 @@
<field name="name">诉讼事项录入表</field>
<field name="approval_type">contentious_matter</field>
</record>
<!-- 天河投资办公室 - 投融资发文 -->
<record id="approval_category_data_tianhe_financing_post_approval" model="approval.category">
<field name="name">投融资发文</field>
<field name="approval_type">financing_post</field>
</record>
<!-- 天河投资办公室 - 业务呈批表 -->
<record id="approval_category_data_tianhe_business_approval" model="approval.category">
<field name="name">业务呈批表</field>
@@ -95,27 +75,27 @@
<!-- 公司通用表单- 补卡 -->
<record id="approval_category_data_generic_forms_replacement_card" model="approval.category">
<field name="name">补卡</field>
<field name="approval_type">generic_forms_replacement_card</field>
<field name="approval_type">card_approval</field>
</record>
<!-- 公司通用表单- 加班 -->
<record id="approval_category_data_generic_forms_overtime" model="approval.category">
<field name="name">加班</field>
<field name="approval_type">generic_forms_overtime</field>
<field name="approval_type">overtime_approval</field>
</record>
<!-- 公司通用表单- 外出 -->
<record id="approval_category_data_generic_forms_outing" model="approval.category">
<field name="name">外出</field>
<field name="approval_type">generic_forms_outing</field>
<field name="approval_type">go_approval</field>
</record>
<!-- 公司通用表单- 销假 -->
<record id="approval_category_data_generic_forms_remove_holiday" model="approval.category">
<field name="name">销假</field>
<field name="approval_type">generic_forms_remove_holiday</field>
<field name="approval_type">remove_holiday_approval</field>
</record>
<!-- 公司通用表单 - 通用离职表 -->
<record id="approval_category_data_tianhe_dimission_approval" model="approval.category">
<field name="name">离职表</field>
<field name="approval_type">dimission</field>
<field name="approval_type">exit_form</field>
</record>
<!-- 公司通用表单 - 试用期转正式员工 -->
<record id="approval_category_data_tianhe_tryout_official_approval" model="approval.category">
@@ -132,12 +112,6 @@
<field name="name">调岗</field>
<field name="approval_type">generic_forms_reassignment</field>
</record>
<!-- 天河投资办公室 - 通用离职表 -->
<record id="approval_category_data_tianhe_dimission_approval" model="approval.category">
<field name="name">离职表</field>
<field name="approval_type">dimission</field>
</record>
<!-- 天河投资办公室 - 试用期转正式员工 -->
<record id="approval_category_data_tianhe_tryout_official_approval" model="approval.category">
<field name="name">试用期转正式员工</field>
@@ -148,11 +122,6 @@
<field name="name">实习期转试用期</field>
<field name="approval_type">exercitation_tryout</field>
</record>
<!-- 天河投资办公室 - 党支委发文 -->
<record id="approval_category_data_tianhe_party_branch_committee_post" model="approval.category">
<field name="name">党支委发文</field>
<field name="approval_type">party_branch_committee_post</field>
</record>
<!-- 天河投资办公室 - 调班登记表 -->
<record id="approval_category_data_tianhe_shift_registration_form" model="approval.category">
<field name="name">调班登记表</field>
@@ -203,10 +172,10 @@
<field name="name">业务呈批表(粮库专用)</field>
<field name="approval_type">liangchu_business_approval_form_grain_depot</field>
</record>
<!-- 粮储 - 总经理办公会议议题呈批表 -->
<!-- 议题申报表 -->
<record id="approval_category_data_liangchu_general_manager_office_meeting_topics" model="approval.category">
<field name="name">总经理办公会议议题呈批表</field>
<field name="approval_type">liangchu_general_manager_office_meeting_topics</field>
<field name="name">议题申报表</field>
<field name="approval_type">issue_library_approval</field>
</record>
<!-- 天河物管 - 产权变更表申请表 -->
<record id="approval_category_data_wuguan_property_right_alter_approval" model="approval.category">
@@ -216,7 +185,7 @@
<!-- 天河物管 - 请假登记单 -->
<record id="approval_category_data_wuguan_leave_registration_approval" model="approval.category">
<field name="name">请假登记单</field>
<field name="approval_type">leave_registration</field>
<field name="approval_type">holiday_approval</field>
</record>
<!-- 天河物管 - 物业管理变更申请表 -->
<record id="approval_category_data_wuguan_property_management_application" model="approval.category">
@@ -243,11 +212,6 @@
<field name="name">跨部门协同</field>
<field name="approval_type">departmental_collaboration</field>
</record>
<!-- 天河物管 - 收文呈批 -->
<record id="approval_category_data_wuguan_receipt_submission" model="approval.category">
<field name="name">收文呈批</field>
<field name="approval_type">receipt_submission</field>
</record>
<!-- 天河投资办公室 - 费用调整申请表 -->
<record id="approval_category_data_tianhe_fee_adjustment_approval" model="approval.category">
<field name="name">费用调整申请表</field>
@@ -258,22 +222,11 @@
<field name="name">费用支出</field>
<field name="approval_type">fee_expend</field>
</record>
<!-- 天河投资办公室 - 发文呈批 -->
<record id="approval_category_data_tianhe_post_submission_approval" model="approval.category">
<field name="name">发文呈批</field>
<field name="approval_type">post_submission</field>
</record>
<!-- 天河投资办公室 - 督办工作表 -->
<record id="approval_category_data_tianhe_supervision_worksheets_approval" model="approval.category">
<field name="name">督办工作表</field>
<field name="approval_type">supervision_worksheets</field>
</record>
<!-- 天河投资办公室 - 公众号/云短信发文呈批 -->
<record id="approval_category_data_ttianhe_official_account_cloud_sms_post_submission_approval"
model="approval.category">
<field name="name">公众号/云短信发文呈批</field>
<field name="approval_type">official_account_cloud_sms_post_submission</field>
</record>
<!-- 天河物管 - 用章申请 -->
<record id="approval_category_data_wuguan_seal_application" model="approval.category">
<field name="name">用章申请</field>
@@ -13,36 +13,39 @@ class ApprovalCategory(models.Model):
('business_statement', '科创中小企业服务中心业务呈批表'),
('process_change', '流程变更申请'),
('contentious_matter', ' 诉讼事项录入表'),
('issue_library_approval', '议题申报表'),
('salary_approval', '工资呈批表'),
('general_party_branch_post', '党总支发文'),
('tianhe_dsh_issues_declarations', '董事会议题申报表'),
('salary', '工资呈批表'),
('party_branch_committee_post', '党支委发文'),
('official_issues_for_son', '公文发文(子企业)'),
('financing_post', '投融资发文'),
('official_account_cloud_sms_post_submission', '公众号/云短信发文呈批'),
('post_submission', '发文呈批'),
('receipt_submission', '收文呈批'),
('official_issues', '公文发文'),
('official_receipt', '公文收文'),
('meeting_file', '会议文档'),
('financing_post', '投融资发文'),
('business', '业务呈批表'),
('property_management_application', '物业管理变更申请表'),
('document_processing', '文件处理表'),
('property_submission', '物业部业务呈批表'),
('rental_receipt', '租赁合同收款单'),
('generic_forms_replacement_card', '公司通用表单——补卡'),
('generic_forms_overtime', '公司通用表单——加班'),
('generic_forms_outing', '公司通用表单——外出'),
('generic_forms_leave', '公司通用表单——请假'),
('generic_forms_duty', '公司通用表单——值班'),
('generic_forms_compensatory', '公司通用表单——调休'),
('generic_forms_remove_holiday', '公司通用表单——销假'),
('dimission', '离职表'),
('card_approval', '补卡审批'),
('overtime_approval', '加班审批'),
('go_approval', '外出审批'),
('holiday_approval', '请假审批'),
('duty_approval', '值班审批'),
('leave_approval', '调休审批'),
('remove_holiday_approval', '销假审批'),
('exit_form', '离职表'),
('tryout_official', '试用期转正式员工'),
('exercitation_tryout', '实习期转试用期'),
('generic_forms_reassignment', '公司通用表单——调岗'),
('party_branch_committee_post', '党支委发文'),
('generic_forms_reassignment', '调岗'),
('shift_registration_form', '调班登记表'),
('meeting_minutes', '会议纪要'),
('engineering', '工程立项'),
('property_right_alter', '产权变更表申请表'),
('leave_registration', '请假登记单'),
('property_management_application', ' '),
('tianhe_property_management_reporting', '物业经营报表管理'),
('tianhe_fire_safety_management', '消防安全管理'),
('tianhe_information_ublish_approval_form', '信息发布呈批表'),
@@ -50,17 +53,13 @@ class ApprovalCategory(models.Model):
('wuguan_intertubular_protocol', '管子间协议'),
('wuguan_invoicing_information_approval_order', '开票信息审批单'),
('liangchu_business_approval_form_grain_depot', '业务呈批表'),
('liangchu_general_manager_office_meeting_topics', '总经理办公会议议题呈批表'),
('liangchu_fixed_assets_purchase_order', '固定资产采购单'),
('wuguan_information_release_approval_form', '信息发布审批表'),
('construction_works', '建设工程审批单'),
('departmental_collaboration', '跨部门协同'),
('receipt_submission', '收文呈批'),
('fee_adjustment', '费用调整申请表'),
('post_submission', '发文呈批'),
('fee_expend', '费用支出'),
('supervision_worksheets', '督办工作表'),
('official_account_cloud_sms_post_submission', '公众号/云短信发文呈批'),
('wuguan_seal_application', '用章申请'),
('wuguan_license_call_batch', '证照调用呈批'),
('wuguan_lease_contract_approval_form', '租赁合同审批单'),
+145
View File
@@ -0,0 +1,145 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
local.conf
poetry.lock
local-addons/*
!local-addons/README.md
!local-addons/__init__.py
.project.yml
.DS_Store
.idea
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+84
View File
@@ -0,0 +1,84 @@
# xy_wechat
适用于Odoo16版本的企业微信模块
<div>
[![GitHub license](https://img.shields.io/github/license/xunonxyz/xy_wechat)](https://github.com/xunonxyz/xy_wechat/blob/master/LICENSE.md)
[![GitHub issues](https://img.shields.io/github/issues/xunonxyz/xy_wechat)](https://github.com/xunonxyz/xy_wechat/issues)
[![Odoo version](https://img.shields.io/badge/Odoo-16-brightgreen)](https://github.com/odoo/odoo/tree/16.0)
</div>
## ✨ 特点
- 采用python升级特性协程进行同步,速度比多线程更快。同步50人公司组织架构只需10s左右
- 仅inherit了少量model,避免多层inherit冗余嵌套影响加载性能
- 支持Odoo多公司切换,每个公司可设置多个企业微信应用
- 使用Hr模块为基础进行员工管理,而不是直接res.users,充分利用Odoo原生模块功能
- 完整的i18n简中翻译,默认使用英文
- 包含企业微信内建应用必备功能,去掉了其余冗余而普遍用不上的功能,减少模块大小
## 🖥 环境支持
- Odoo16版本
- postgresql12及以上版本(Odoo16的hr模块中使用了Json字段,需要数据库有Json函数支持)
- python3.7及以上(使用了协程特性)
- 安装python的`aiohttp`和`pycrypto`库
## 📦 安装
在Odoo的应用里找到`Wechat Enterprise`模块激活即可安装,安装后模块名称翻译会被加载变成「企业微信」
## ✨ 包含功能
| 功能名称 | 是否支持 |
| :-----------------------------------: | :------: |
| 多公司用户切换 | √ |
| 企业微信应用access_token获取并缓存 | √ |
| WW_verify_xxx.txt域名校验文件一键配置 | √ |
| 企业微信应用通讯录部门、成员一键同步 | √ |
| 企业微信通讯录部门、成员变更回调 | √ |
| 企业微信二维码扫码登录系统 | √ |
| 企业微信网页跳转登录系统 | √ |
| 向企业微信上传临时素材、永久图片 | √ |
| 推送、撤回企业微信各类型消息 | √ |
## 🔨 使用
### 配置企业微信应用参数
在「Odoo系统 - 设置 - 用户&公司」页面,选中公司后切换到「企业微信应用」选项卡进行参数配置
注:如果勾选了应用中的「同步员工到res.user」则在保存hr.employee记录时也会创建res.users记录,否则仅创建hr.employee
![./static/img/setting.png](./static/img/setting.png)
### 同步
配置完参数后点击「同步组织架构」按钮即可。同步是在后端异步进行,不会阻塞当前操作,但尽量不要进行用户、部门的变更、创建、删除等操作。
同步完成或失败后均会有通知窗进行通知。
![./static/img/notice.png](./static/img/notice.png)
### 同步日志
每次同步的结果、报错代码栈、花费的时间等信息均可以进行查看。
在「员工 - 企业微信模块日志」页面可进行查看
![./static/img/log.png](./static/img/log.png)
## ❤️ 支持
富能通企业官网:https://www.funenc.com/
免费后端培训视频系列:https://www.bilibili.com/video/BV1rP411G7CZ/
知识星球Odoo源码解读:
<img src="./static/img/knowledge_star.png" style="width: 300px">
更多模块或进群交流欢迎加微信:
<img src="./static/img/wx_qrcode.png" style="width: 200px">
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
+41
View File
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
{
'name': "Wechat Enterprise",
'summary': 'Wechat Enterprise',
'description': """
Api and methods for Wechat Enterprise , which is used in Internal Enterprise Applications.
Use pycrypto to replace Crypto.
Need python version >= 3.7
""",
'author': "funenc",
'website': "https://www.funenc.com",
'external_dependencies': {
'python': ['aiohttp', 'Crypto'],
},
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'hr'],
'assets': {
'web.assets_backend': [
'wechat_enterprise/static/src/js/we_qrcode_widget.js',
]
},
# always loaded
'data': [
'security/ir.model.access.csv',
'views/company.xml',
'views/app.xml',
'views/log.xml',
],
'application': True
}
+4
View File
@@ -0,0 +1,4 @@
from . import token_store
from . import we_request
from . import custom_encrypt
from . import callback
@@ -0,0 +1,280 @@
#!/usr/bin/env python
# -*- encoding:utf-8 -*-
""" 对企业微信发送给企业后台的消息加解密示例代码.
@copyright: Copyright (c) 1998-2014 Tencent Inc.
"""
# ------------------------------------------------------------------------
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
from . import ierror
"""
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
"""
class FormatException(Exception):
pass
def throw_exception(message, exception_class=FormatException):
"""my define raise exception function"""
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 ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.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 ierror.WXBizMsgCrypt_OK, encrypt.text
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_ParseXml_Error, 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])
try:
ciphertext = cryptor.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
def decrypt(self, text, receiveid):
"""对解密后的明文进行补位删除
@param text: 密文
@return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key, self.mode, self.key[:16])
# 使用BASE64对密文进行解码,然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.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 ierror.WXBizMsgCrypt_IllegalBuffer, None
if from_receiveid.decode('utf8') != receiveid:
return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None
return 0, xml_content
def get_random_str(self):
""" 随机生成16位字符串
@return: 16位字符串
"""
return str(random.randint(1000000000000000, 9999999999999999)).encode()
class WXBizMsgCrypt(object):
# 构造函数
def __init__(self, sToken, sEncodingAESKey, sReceiveId):
try:
self.key = base64.b64decode(sEncodingAESKey + "=")
assert len(self.key) == 32
except:
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
self.m_sToken = sToken
self.m_sReceiveId = sReceiveId
# 验证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,失败返回对应的错误码
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.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')
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)
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 ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId)
return ret, xml_content
@@ -0,0 +1,2 @@
from . import ierror
from . import WXBizMsgCrypt
@@ -0,0 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# 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
+28
View File
@@ -0,0 +1,28 @@
import hashlib
import random
salt_list = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def get_random_str():
salt = ''
for i in range(16):
salt += random.choice(salt_list)
return salt
class CustomEncrypt(object):
"""
custom encrypt, use md5 to encrypt password, and add salt to password. has not decrypt function
"""
salt = get_random_str()
@staticmethod
def encrypt(password):
obj = hashlib.md5(CustomEncrypt.salt.encode("utf-8"))
obj.update(password.encode('utf-8'))
return obj.hexdigest()
@staticmethod
def is_equal(password, encode_password):
return CustomEncrypt.encrypt(password) == encode_password
+70
View File
@@ -0,0 +1,70 @@
import time
token_temp = {}
class TokenStore(object):
def __init__(self, secret):
"""
secret is used to distinguish different WE apps
:param secret:
"""
self.secret = secret
def save(self, token, expires_in, create_time=None):
"""
save token
:param token: token
:param expires_in: how long the token is valid, unit is second
:param create_time: if create_time is None, use current time
:return:
"""
current_time = time.time()
token_temp[self.secret] = {
'token': token,
'expires_in': expires_in,
'update_time': current_time,
'create_time': create_time or current_time
}
def get(self):
"""
if token is expires, clear it and return None
:return:
"""
if self.secret in token_temp:
token = token_temp[self.secret]
if time.time() - token['update_time'] > token['expires_in']:
token_temp.pop(self.secret)
return None
return token['token']
return None
def refresh(self, expires_in):
"""
refresh token if it exists
:param expires_in: how long the token is valid, unit is second
:return:
"""
if self.secret in token_temp:
token_dict = token_temp[self.secret]
self.save(token_dict['token'], expires_in, token_dict['create_time'])
@staticmethod
def clean(secret):
"""
clean token by secret
:param secret: string
:return:
"""
if secret in token_temp:
token_temp.pop(secret)
@staticmethod
def clean_all():
"""
clean all token
:return:
"""
for key in token_temp:
token_temp.pop(key)
+228
View File
@@ -0,0 +1,228 @@
import aiohttp
from urllib import parse
from .token_store import TokenStore
def check_response_error(response, error_code=0, error_msg_key='errmsg'):
if response['errcode'] != error_code:
raise Exception(response[error_msg_key])
def join_url(base_url, *args):
if not args:
return base_url
return parse.urljoin(base_url, ''.join(args))
class WeRequest(object):
url_prefix = 'https://qyapi.weixin.qq.com/cgi-bin/'
def __init__(self, corp_id, secret):
"""
set WE corp_id and secret
:param corp_id: WE app corp_id
:param secret: WE app secret
"""
self.corp_id = corp_id
self.secret = secret
self.token_store = TokenStore(secret)
async def refresh_token(self):
"""
refresh token if it expires
:return:
"""
current_token = self.token_store.get()
if not current_token:
token = await self.get_token()
self.token_store.save(token['token'], token['expires_in'])
async def latest_token(self):
"""
get latest token
:return:
"""
await self.refresh_token()
return self.token_store.get()
@staticmethod
async def get_response(url, params=None, response_callback=None):
"""
get response from server
:param url: url join with url_prefix
:param params:
:param response_callback: response callback function
:return:
"""
conn = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=conn) as session:
async with session.get(url, params=params) as response:
return await response_callback(response) if response_callback else await response.json()
@staticmethod
async def post_response(url, json, data=None, response_callback=None):
"""
post response to server, if json is not None, use json, else use data
:param url: url join with url_prefix
:param data: json data
:param json: form data
:param response_callback: response callback function
:return:
"""
conn = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=conn) as session:
async with session.post(url, json=json, data=data) as response:
return await response_callback(response) if response_callback else await response.json()
async def get_token(self):
"""
get token from server
:return:
"""
response = await self.get_response(join_url(self.url_prefix, 'gettoken'), {
'corpid': self.corp_id,
'corpsecret': self.secret
})
check_response_error(response)
return {
'token': response['access_token'],
'expires_in': response['expires_in']
}
async def department_simplelist(self, id=None):
"""
get department simplelist from server
:param id: parent department id, if None, get all department
:return:
"""
response = await self.get_response(join_url(self.url_prefix, 'department/simplelist'), {
'access_token': await self.latest_token(),
'id': id or ''
})
check_response_error(response)
return response['department_id']
async def department_detail(self, id):
"""
get department detail from server
:param id: department id, required
:return:
"""
assert id, 'id is required'
response = await self.get_response(join_url(self.url_prefix, 'department/get'), {
'access_token': await self.latest_token(),
'id': id
})
check_response_error(response)
return response['department']
async def department_users(self, dep_id):
"""
get department users from server
:param dep_id: department id, required
:return:
"""
assert dep_id, 'dep_id is required'
response = await self.get_response(join_url(self.url_prefix, 'user/list'), {
'access_token': await self.latest_token(),
'department_id': dep_id
})
check_response_error(response)
return response['userlist']
async def get_userid(self, code):
"""
get userid from server
:param code: user code by scan qrcode
:return:
"""
assert code, 'code is required'
response = await self.get_response(join_url(self.url_prefix, 'auth/getuserinfo'), {
'access_token': await self.latest_token(),
'code': code
})
check_response_error(response)
return response.get('userid', None) or response.get('openid', None)
async def send_message(self, message):
"""
send message to user
:param message: message dict
:return:
"""
assert message and isinstance(message, dict), 'message is required and must be dict'
response = await self.post_response(
join_url(self.url_prefix, f'message/send?access_token={await self.latest_token()}'), message)
check_response_error(response)
return response.get('msgid', None)
async def recall_message(self, msg_id):
"""
recall message
:param msg_id: message id
:return:
"""
assert msg_id, 'msg_id is required'
response = await self.post_response(
join_url(self.url_prefix, f'message/recall?access_token={await self.latest_token()}'), {'msgid': msg_id})
check_response_error(response)
return response.get('errmsg', None)
async def upload_media(self, media_type, media_file, filename):
"""
upload temporary media to server, it only save 3 days, the file size has limit,
reference: https://developer.work.weixin.qq.com/document/path/91054
:param media_type: image, voice, video, file
:param media_file: file content bytes
:param filename: file name
:return: media_id
"""
data = aiohttp.FormData()
data.add_field('media', media_file, filename=filename, content_type='application/octet-stream')
response = await self.post_response(
join_url(self.url_prefix, f'media/upload?access_token={await self.latest_token()}&type={media_type}'),
None, data)
check_response_error(response)
return response.get('media_id', None)
async def upload_image(self, media_file, filename):
"""
upload image to server, it can save permanently, the file size has limit,
reference: https://developer.work.weixin.qq.com/document/path/91054
:param media_file: file content bytes
:param filename: file name
:return: file url
"""
data = aiohttp.FormData()
data.add_field(filename, media_file, filename=filename, content_type='application/octet-stream')
response = await self.post_response(
join_url(self.url_prefix, f'media/uploadimg?access_token={await self.latest_token()}'),
None, data)
check_response_error(response)
return response.get('url', None)
async def get_media(self, media_id):
"""
get media from server
:param media_id: media id
:return: media content bytes
"""
async def _cb(res):
return await res.read()
response = await self.get_response(join_url(self.url_prefix, f'media/get'), {
'access_token': await self.latest_token(),
'media_id': media_id
}, response_callback=_cb)
return response
def we_request_instance(corp_id, secret):
"""
if you want to use custom WeRequest class or Store class, you can set monkey patch to this function
:param corp_id:
:param secret:
:return:
"""
return WeRequest(corp_id, secret)
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers
+145
View File
@@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
import asyncio
import base64
from urllib import parse
import warnings
import xml.etree.cElementTree as ET
from odoo import http
from odoo.http import route, request
from ..common.we_request import we_request_instance, join_url
from ..models.res_users import ResUsers
from ..common.custom_encrypt import CustomEncrypt
from ..common.callback.WXBizMsgCrypt import WXBizMsgCrypt
# Crypto will show warning, ignore it
warnings.filterwarnings("ignore", category=DeprecationWarning)
class WechatEnterprise(http.Controller):
scan_oauth_url = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid={corp_id}&agentid={agentid}&redirect_uri={redirect_uri}'
web_oauth_url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid={corp_id}&redirect_uri={redirect_uri}&response_type=code&scope={scope}&agentid={agentid}#wechat_redirect'
@http.route('/WW_verify_<string:filename>.txt', type='http', auth="none", methods=['GET'])
def load_file(self, filename=None):
"""
load verify file, easily to verify wechat enterprise redirect url
:param filename:
:return:
"""
app = request.env['wechat.enterprise.app'].sudo().search(
[('verify_txt_filename', '=', f'WW_verify_{filename}.txt')],
limit=1
)
if len(app) == 0:
content = None
else:
content = base64.b64decode(app[0].verify_txt).decode('utf-8')
return content
@route('/we/oauth2/info', type='json', auth='public')
def get_we_oauth2_info(self, app_id, redirect_uri=None):
"""
get info for build Wechat Enterprise QRCode to login
:param app_id:
:param redirect_uri: redirect url after login, don't need host
:return:
"""
app = request.env['wechat.enterprise.app'].sudo().browse(int(app_id))
host = request.httprequest.host_url
return {
'agentid': app.agentid,
'corp_id': app.corp_id,
'redirect_uri': join_url(host, f'we/oauth2/login/{app_id}') if redirect_uri is None else
join_url(host, redirect_uri)
}
@route('/we/oauth2/login/<int:app_id>', type='http', auth='public')
def login_by_oauth2(self, code, app_id):
"""
login by wechat enterprise oauth2
:param code:
:param app_id:
:return:
"""
app = request.env['wechat.enterprise.app'].sudo().browse(int(app_id))
we_request = we_request_instance(app.corp_id, app.secret)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
get_userid_task = loop.create_task(we_request.get_userid(code))
loop.run_until_complete(get_userid_task)
loop.close()
we_id = get_userid_task.result()
employee = request.env['hr.employee'].sudo().search([('we_id', '=', we_id)])
if employee.user_id.id:
secret_dict = {
'npa': True,
'type': 'we',
'password': CustomEncrypt.encrypt(ResUsers.we_auth_secret)
}
request.session.authenticate(request.session.db, employee.user_id.login, secret_dict)
return request.redirect('/web')
@route('/we/oauth2/build/<string:oauth_type>/<int:app_id>', type='http', auth='public')
def we_oauth2(self, oauth_type, app_id, scope='snsapi_base'):
"""
build wechat enterprise oauth2 url
:param oauth_type: scan or web, scan is build to qrcode, web is build to redirect url
:param app_id: authenticate used app id
:param scope: snsapi_base or snsapi_privateinfo
:return:
"""
oauth2_info = self.get_we_oauth2_info(app_id)
corp_id = oauth2_info['corp_id']
agentid = oauth2_info['agentid']
redirect_uri = parse.quote(oauth2_info['redirect_uri'])
_redirect_uri = ''
if oauth_type == 'scan':
_redirect_uri = WechatEnterprise.scan_oauth_url.format(
corp_id=corp_id,
agentid=agentid,
redirect_uri=redirect_uri
)
elif oauth_type == 'web':
_redirect_uri = WechatEnterprise.web_oauth_url.format(
corp_id=corp_id,
agentid=agentid,
redirect_uri=redirect_uri,
scope=scope
)
return request.redirect(_redirect_uri, 303, False)
@route('/we/mail_list/callback', type='http', auth='public', csrf=False)
def we_callback(self, msg_signature, timestamp, nonce, echostr=''):
if request.httprequest.method == 'GET':
result = None
company_list = request.env['res.company'].sudo().search([])
for company in company_list:
if company.we_cb_token and company.we_cb_encoding_AES_key:
wxcpt = WXBizMsgCrypt(company.we_cb_token, company.we_cb_encoding_AES_key, company.we_corp_id)
ret, s_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if ret == 0:
result = s_echo_str
break
return result
else:
data = request.httprequest.data.decode('utf-8')
# get ToUserName to match company corp_id
temp_xml_tree = ET.fromstring(data)
corp_id = temp_xml_tree.find('ToUserName').text
company = request.env['res.company'].sudo().search([('we_corp_id', '=', corp_id)])
wxcpt = WXBizMsgCrypt(company.we_cb_token, company.we_cb_encoding_AES_key, company.we_corp_id)
ret, result = wxcpt.DecryptMsg(data, msg_signature, timestamp, nonce)
if ret == 0:
xml_tree = ET.fromstring(result)
change_type = xml_tree.find('ChangeType').text
# change_type may be xxx_user or xxx_party
suffix = change_type.split('_')[1]
model = request.env['hr.employee'] if suffix == 'user' else request.env['hr.department']
# method need return 'success' or empty string
func = getattr(model, f'on_we_{change_type}', None)
if func:
return func(xml_tree, company)
return ''
+235
View File
@@ -0,0 +1,235 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * wechat_enterprise
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-08 09:36+0000\n"
"PO-Revision-Date: 2022-11-08 09:36+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: base
#: model:ir.module.module,shortdesc:base.module_wechat_enterprise
#: model:ir.module.module,summary:base.module_wechat_enterprise
msgid "Wechat Enterprise"
msgstr "企业微信"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__agentid
msgid "Agent ID"
msgstr "应用ID"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_res_company__app_ids
msgid "Apps"
msgstr "企业微信应用"
#. module: wechat_enterprise
#: model_terms:ir.ui.view,arch_db:wechat_enterprise.we_company_extend
msgid "Callback Settings"
msgstr "通讯录回调设置"
#. module: wechat_enterprise
#: model:ir.model,name:wechat_enterprise.model_res_company
msgid "Companies"
msgstr "公司"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__company_id
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__company_id
msgid "Company"
msgstr "公司"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_res_company__we_corp_id
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__corp_id
msgid "Corp ID"
msgstr "企业ID"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__create_uid
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__create_uid
msgid "Created by"
msgstr "创建人"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__create_date
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__create_date
msgid "Created on"
msgstr "创建时间"
#. module: wechat_enterprise
#: model:ir.model,name:wechat_enterprise.model_hr_department
msgid "Department"
msgstr "部门"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__description
msgid "Description"
msgstr "说明"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__detail
msgid "Detail"
msgstr "详情"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__display_name
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__display_name
msgid "Display Name"
msgstr "显示名称"
#. module: wechat_enterprise
#: model:ir.model,name:wechat_enterprise.model_hr_employee
msgid "Employee"
msgstr "员工"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_res_company__we_cb_encoding_AES_key
msgid "EncodingAESKey"
msgstr ""
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__id
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__id
msgid "ID"
msgstr ""
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app____last_update
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log____last_update
msgid "Last Modified on"
msgstr "最后修改时间"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__write_uid
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__write_uid
msgid "Last Updated by"
msgstr "最后更新人"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__write_date
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__write_date
msgid "Last Updated on"
msgstr "最后更新时间"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__name
msgid "Name"
msgstr "名称"
#. module: wechat_enterprise
#: code:addons/wechat_enterprise/models/employee.py:0
#, python-format
msgid "Please select the user or department to send the message!"
msgstr "请选择发送message的用户或部门!"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__secret
msgid "Secret"
msgstr ""
#. module: wechat_enterprise
#: code:addons/wechat_enterprise/models/app.py:0
#, python-format
msgid "Start sync organization now, please wait......"
msgstr "开始同步组织架构,请稍候......"
#. module: wechat_enterprise
#: model_terms:ir.ui.view,arch_db:wechat_enterprise.we_company_extend
msgid "Sync Organization"
msgstr "同步组织架构"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__sync_with_user
msgid "Sync with res.user"
msgstr "同步员工到res.user"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_res_company__we_cb_token
msgid "Token"
msgstr ""
#. module: wechat_enterprise
#: model_terms:ir.ui.view,arch_db:wechat_enterprise.wechat_enterprise_app_form
msgid "Url Verify Settings"
msgstr "域名验证设置"
#. module: wechat_enterprise
#: model:ir.model,name:wechat_enterprise.model_res_users
msgid "User"
msgstr "用户"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__verify_txt_filename
msgid "Verify Txt Filename"
msgstr "网页授权验证文件名"
#. module: wechat_enterprise
#: model:ir.model,name:wechat_enterprise.model_wechat_enterprise_app
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_log__we_app_id
msgid "Wechat Enterprise App"
msgstr "企业微信应用"
#. module: wechat_enterprise
#: model_terms:ir.ui.view,arch_db:wechat_enterprise.we_company_extend
msgid "Wechat Enterprise Apps"
msgstr "企业微信应用"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_hr_department__we_id
msgid "Wechat Enterprise Department ID"
msgstr "企业微信部门ID"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_hr_department__we_order
msgid "Wechat Enterprise Department Order"
msgstr "企业微信部门排序"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_hr_employee__we_department_ids
msgid "Wechat Enterprise Departments"
msgstr "企业微信部门"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_hr_department__we_employee_ids
msgid "Wechat Enterprise Employees"
msgstr "企业微信员工"
#. module: wechat_enterprise
#: model:ir.model,name:wechat_enterprise.model_wechat_enterprise_log
msgid "Wechat Enterprise Log"
msgstr "企业微信日志"
#. module: wechat_enterprise
#: model:ir.actions.act_window,name:wechat_enterprise.wechat_enterprise_log_act_window
#: model:ir.ui.menu,name:wechat_enterprise.menu_wechat_enterprise_log
msgid "Wechat Enterprise Logs"
msgstr "企业微信模块日志"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_hr_department__we_parent_id
msgid "Wechat Enterprise Parent Department ID"
msgstr "企业微信父部门ID"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_hr_employee__we_extattr
msgid "Wechat Enterprise User Extattr"
msgstr "企业微信用户扩展属性"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_hr_employee__we_id
msgid "Wechat Enterprise User ID"
msgstr "企业微信用户ID"
#. module: wechat_enterprise
#: model:ir.model.fields,field_description:wechat_enterprise.field_wechat_enterprise_app__verify_txt
msgid "verify_txt"
msgstr "网页授权验证文件"
+8
View File
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from . import app
from . import company
from . import department
from . import employee
from . import log
from . import res_users
+129
View File
@@ -0,0 +1,129 @@
import asyncio
import threading
import time
import datetime
import traceback
from odoo import models, fields, api
from odoo.tools.translate import _
from ..common.we_request import we_request_instance
def get_now_time_str():
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
class App(models.Model):
_name = 'wechat.enterprise.app'
_description = 'Wechat Enterprise App'
name = fields.Char(string='Name', required=True)
description = fields.Text(string='Description')
corp_id = fields.Char(string='Corp ID', related='company_id.we_corp_id', readonly=True)
secret = fields.Char(string='Secret', required=True)
agentid = fields.Char(string='Agent ID', required=True)
sync_with_user = fields.Boolean(string='Sync with res.user', default=True)
company_id = fields.Many2one('res.company', string='Company', required=True)
# use in verify url
verify_txt_filename = fields.Char(string='Verify Txt Filename', readonly=True)
verify_txt = fields.Binary('verify_txt')
def run_sync(self):
self.env['bus.bus']._sendone(self.env.user.partner_id, 'simple_notification', {
'title': 'Sync Start......',
'message': _('Start sync organization now, please wait......'),
'warning': True
})
# create a threading to avoid odoo ui blocking
def _sync():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
asyncio.run(self.sync_organization())
thread = threading.Thread(target=_sync)
thread.start()
async def sync_organization(self):
start = time.time()
uid = self.env.uid
is_success = True
with self.env.registry.cursor() as new_cr:
self.env = api.Environment(new_cr, uid, self.env.context)
detail_log = f'start sync at {get_now_time_str()}......'
try:
we_request = we_request_instance(self.corp_id, self.secret)
await self.env['hr.department'].with_context(
self.env.context, we_app=self, we_request=we_request
).sync_department()
detail_log += f'\nsync success!'
except Exception:
is_success = False
detail_log += f'\nsync failed, error: \n{traceback.format_exc()}'
finally:
detail_log += f'\nsync end at {get_now_time_str()}, cost {round(time.time() - start, 2)}s'
company_id = self.company_id.id
self.env['wechat.enterprise.log'].create({
'company_id': company_id,
'we_app_id': self.id,
'detail': detail_log
})
self.env['bus.bus']._sendone(self.env.user.partner_id, 'simple_notification', {
'title': 'Sync End......',
'message': f'Sync organization end, {"success" if is_success else "failed"}',
'warning': True if is_success else False
})
def upload_media(self, app_id, media_type, file_content, filename):
"""
upload temp media to wechat server
:param app_id: save at which app server
:param media_type: image, voice, video, file
:param file_content: file content bytes
:param filename: file name
:return: media_id in wechat server
"""
app = self.env['wechat.enterprise.app'].sudo().browse(int(app_id))
we_request = we_request_instance(app.corp_id, app.secret)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
upload_media_task = loop.create_task(we_request.upload_media(media_type, file_content, filename))
loop.run_until_complete(upload_media_task)
loop.close()
return upload_media_task.result()
def upload_image(self, app_id, file_content, filename):
"""
upload permanently image to wechat server
:param app_id: save at which app server
:param file_content: file content bytes
:param filename: file name
:return: file_url in wechat server
"""
app = self.env['wechat.enterprise.app'].sudo().browse(int(app_id))
we_request = we_request_instance(app.corp_id, app.secret)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
upload_img_task = loop.create_task(we_request.upload_image(file_content, filename))
loop.run_until_complete(upload_img_task)
loop.close()
return upload_img_task.result()
def get_media(self, app_id, media_id):
"""
get media from wechat server
:param app_id: save in which app server
:param media_id: media id in wechat server
:return: file content bytes
"""
app = self.env['wechat.enterprise.app'].sudo().browse(int(app_id))
we_request = we_request_instance(app.corp_id, app.secret)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
get_media_task = loop.create_task(we_request.get_media(media_id))
loop.run_until_complete(get_media_task)
loop.close()
return get_media_task.result()
+11
View File
@@ -0,0 +1,11 @@
from odoo import models, fields
class Company(models.Model):
_inherit = 'res.company'
we_corp_id = fields.Char(string='Corp ID', required=True, default='Wechat Enterprise corp id')
app_ids = fields.One2many('wechat.enterprise.app', 'company_id', string='Apps')
# use in WE callback
we_cb_token = fields.Char(string='Token')
we_cb_encoding_AES_key = fields.Char(string='EncodingAESKey')
+137
View File
@@ -0,0 +1,137 @@
import asyncio
from odoo import models, fields
class Department(models.Model):
_inherit = 'hr.department'
we_id = fields.Char(string='Wechat Enterprise Department ID')
we_parent_id = fields.Char(string='Wechat Enterprise Parent Department ID')
we_order = fields.Integer(string='Wechat Enterprise Department Order')
'''because a Wechat Enterprise user can have multi departments, so we need a many2many field,
the department_id field is only main department'''
we_employee_ids = fields.Many2many('hr.employee', 'we_employee_department_rel', 'department_id', 'employee_id',
string='Wechat Enterprise Employees')
async def get_server_depart_tree(self):
"""
get Wechat Enterprise server department tree, root parentid is 1
:return:
"""
we_request = self.env.context.get('we_request')
departments = await we_request.department_simplelist()
def get_tree_data(arr, parent_id):
tree = []
for item in arr:
if item['parentid'] == parent_id:
tree.append({
'id': item['id'],
'parentid': item['parentid'],
'order': item['order'],
'children': get_tree_data(arr, item['id'])
})
return tree
return get_tree_data(departments, 1)
async def sync_department(self):
"""
sync department from wechat enterprise
:return:
"""
we_request = self.env.context.get('we_request')
we_app = self.env.context.get('we_app')
depart_tree = await self.get_server_depart_tree()
tasks = []
async def _sync_dep(server_dep, parent_id):
_tasks = []
dep_detail = await we_request.department_detail(server_dep['id'])
dep = self.search([('we_id', '=', dep_detail['id'])])
# dep need commit to db because sync user need use it
modify_data = {
'company_id': we_app.company_id.id,
'name': dep_detail['name'],
'we_id': dep_detail['id'],
'parent_id': parent_id,
'we_parent_id': dep_detail['parentid'],
'we_order': dep_detail['order'],
'manager_id': False
}
if dep.id is False:
dep = self.create(modify_data)
else:
# change where the employee in the department status to active = False
self.env['hr.employee'].search([('we_department_ids.we_id', '=', dep_detail['id'])]).write(
{'active': False})
dep.write(modify_data)
await self.env['hr.employee'].sync_user(dep, dep_detail['id'])
if len(server_dep['children']) > 0:
for child in server_dep['children']:
_tasks.append(_sync_dep(child, dep.id))
await asyncio.gather(*_tasks)
for department in depart_tree:
tasks.append(_sync_dep(department, False))
await asyncio.gather(*tasks)
def on_we_create_party(self, xml_tree, company):
"""
on wechat enterprise create department event, only has Id and ParentId, no Name or Other fields
:param xml_tree: ET.fromstring(str) instance
:param company: receive company
:return:
"""
we_id = xml_tree.find('Id').text
we_parent_id = xml_tree.find('ParentId').text
parent_dep = self.sudo().search([('we_id', '=', we_parent_id), ('company_id', '=', company.id)])
self.sudo().create({
'company_id': company.id,
'name': f'we_id_{we_id}',
'we_id': we_id,
'parent_id': parent_dep.id,
'we_parent_id': we_parent_id,
'manager_id': False
})
return 'success'
def on_we_update_party(self, xml_tree, company):
"""
on wechat enterprise update department event, only has Id and ParentId, no Name or Other fields
:param xml_tree: ET.fromstring(str) instance
:param company: receive company
:return:
"""
we_id = xml_tree.find('Id').text
we_parent_id = xml_tree.find('ParentId').text
dep = self.sudo().search([('we_id', '=', we_id), ('company_id', '=', company.id)])
if dep.id is not False:
parent_dep = self.sudo().search([('we_id', '=', we_parent_id), ('company_id', '=', company.id)])
dep.write({
'parent_id': parent_dep.id,
'we_parent_id': we_parent_id
})
return 'success'
def on_we_delete_party(self, xml_tree, company):
"""
on wechat enterprise delete department event, only has Id and ParentId, no Name or Other fields
:param xml_tree: ET.fromstring(str) instance
:param company: receive company
:return:
"""
we_id = xml_tree.find('Id').text
dep = self.sudo().search([('we_id', '=', we_id), ('company_id', '=', company.id)])
if dep.id is not False:
dep.unlink()
return 'success'
+234
View File
@@ -0,0 +1,234 @@
import asyncio
from odoo import models, fields
from odoo.exceptions import UserError
from odoo.tools.translate import _
from ..common.we_request import we_request_instance
USER_STATUS = {
'active': [1, 4],
'not_active': [2, 5]
}
def send_list_to_str(send_list):
"""
convert send list to str
:param send_list: wx_id list or string of @all
:return:
"""
if send_list == 'all' or send_list is None:
return send_list
return '|'.join(send_list)
class Employee(models.Model):
_inherit = 'hr.employee'
we_id = fields.Char(string='Wechat Enterprise User ID')
we_department_ids = fields.Many2many('hr.department', 'we_employee_department_rel', 'employee_id', 'department_id',
string='Wechat Enterprise Departments')
we_extattr = fields.Json(string='Wechat Enterprise User Extattr')
def write_with_user(self, val):
if self.user_id.id is False:
user = self.env['res.users'].create({
'name': val['name'],
'login': val['we_id'],
'company_id': val['company_id'],
'company_ids': [(4, val['company_id'])],
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
'active': val['active']
})
val['user_id'] = user.id
else:
self.user_id.write({
'name': val['name'],
'active': val['active']
})
self.write(val)
def create_with_user(self, val_list):
for val in val_list:
user = self.env['res.users'].search([('login', '=', val['we_id']), ('active', 'in', [True, False])])
if user.id is False:
user = self.env['res.users'].create({
'name': val['name'],
'login': val['we_id'],
'company_id': val['company_id'],
'company_ids': [(4, val['company_id'])],
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
'active': val['active']
})
val['user_id'] = user.id
return self.create(val_list)
async def sync_user(self, we_department, server_dep_id):
we_request = self.env.context.get('we_request')
we_app = self.env.context.get('we_app')
sync_with_user = we_app.sync_with_user
user_list = await we_request.department_users(server_dep_id)
create_users = []
manager_id = None
for user in user_list:
# job
job = self.env['hr.job'].search(
[('name', '=', user['position']), ('company_id', '=', we_app.company_id.id)])
if job.id is False:
job = self.env['hr.job'].create({
'name': user['position'],
'company_id': we_app.company_id.id
})
employee = self.search([('we_id', '=', user['userid']), ('active', 'in', [True, False])])
main_department = we_department.search([('we_id', '=', user['main_department'])])
modify_data = {
'name': user['name'],
'we_id': user['userid'],
'company_id': we_app.company_id.id,
'department_id': main_department.id,
'we_department_ids': [(4, we_department.id)],
'job_id': job.id,
'we_extattr': user['extattr'],
'active': user['status'] in USER_STATUS['active']
}
if employee.id is False:
modify_data['marital'] = False
modify_data['work_phone'] = False
modify_data['parent_id'] = False
create_users.append(modify_data)
else:
employee.write_with_user(modify_data) if sync_with_user else employee.write(modify_data)
# set department manager
if user['isleader'] == 1 and not manager_id:
manager_id = user['userid']
if len(create_users) > 0:
# create users limit 500
limit = 500
for i in range(0, len(create_users), limit):
create_vals = create_users[i:i + limit]
# if config set not sync user, not create user
self.create_with_user(create_vals) if sync_with_user else self.create(create_vals)
if manager_id:
we_department.write({'manager_id': self.search([('we_id', '=', manager_id)]).id})
def send_we_message(self, app_id, to_users, to_departments=None, msgtype='text', **kwargs):
"""
send message in Wechat Enterprise
because sync's info has no tag, so we can't send message to tag, only can send to the single app,
the existing parameters are 'to_users', 'to_departments', 'msgtype', 'agentid'
:param app_id: wechat enterprise app id used to send message
:param to_users: wechat enterprise user we_id list, if to all user, set to '@all'
:param to_departments: wechat enterprise department we_id list, if to all department, set to '@all'
:param msgtype: message type, reference https://developer.work.weixin.qq.com/document/path/90236
:param kwargs: other parameters, reference https://developer.work.weixin.qq.com/document/path/90236
:return: message id
"""
assert app_id, 'app_id is required'
if len(to_users) == 0 and len(to_departments) == 0:
raise UserError(_('Please select the user or department to send the message!'))
app = self.env['wechat.enterprise.app'].sudo().browse(int(app_id))
we_request = we_request_instance(app.corp_id, app.secret)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
send_message_task = loop.create_task(we_request.send_message(dict(
touser=send_list_to_str(to_users),
toparty=send_list_to_str(to_departments),
msgtype=msgtype,
agentid=app.agentid,
**kwargs
)))
loop.run_until_complete(send_message_task)
loop.close()
return send_message_task.result()
def recall_we_message(self, app_id, msg_id):
"""
recall message in Wechat Enterprise
:param app_id: wechat enterprise app id used to recall message
:param msg_id: Has been sent message id
:return: True or False
"""
assert app_id, 'app_id is required'
assert msg_id, 'msg_id is required'
app = self.env['wechat.enterprise.app'].sudo().browse(int(app_id))
we_request = we_request_instance(app.corp_id, app.secret)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
recall_message_task = loop.create_task(we_request.recall_message(msg_id))
loop.run_until_complete(recall_message_task)
loop.close()
return recall_message_task.result() == 'ok'
def on_we_create_user(self, xml_tree, company):
"""
on wechat enterprise create user event, only has UserID and Department, no Other fields
:param xml_tree: ET.fromstring(str) instance
:param company: receive company
:return:
"""
we_id = xml_tree.find('UserID').text
# multi dep_id is split by ','
we_dep_id = xml_tree.find('Department').text
dep_id_list = we_dep_id.split(',')
we_departments = self.env['hr.department'].sudo().search(
[('we_id', 'in', dep_id_list), ('company_id', '=', company.id)])
self.sudo().create({
'name': f'we_id_{we_id}',
'we_id': we_id,
'company_id': company.id,
# use first department as main department temporarily
'department_id': we_departments[0].id if len(we_departments) > 0 else False,
'we_department_ids': [(6, 0, we_departments.ids)],
'active': True
})
return 'success'
def on_we_update_user(self, xml_tree, company):
"""
on wechat enterprise update user event, only has UserID and Department and NewUserId, no Other fields,
only trigger when user change department or user_id
:param xml_tree: ET.fromstring(str) instance
:param company: receive company
:return:
"""
we_id = xml_tree.find('UserID').text
new_we_id = xml_tree.find('NewUserId').text if xml_tree.find('NewUserId') else None
# multi dep_id is split by ','
we_dep_id = xml_tree.find('Department').text
dep_id_list = we_dep_id.split(',')
we_departments = self.env['hr.department'].sudo().search(
[('we_id', 'in', dep_id_list), ('company_id', '=', company.id)])
employee = self.sudo().search([('we_id', '=', we_id), ('company_id', '=', company.id)])
if employee.id is not False:
employee.write({
'we_id': new_we_id or we_id,
# use first department as main department temporarily
'department_id': we_departments[0].id if len(we_departments) > 0 else False,
'we_department_ids': [(6, 0, we_departments.ids)]
})
return 'success'
def on_we_delete_user(self, xml_tree, company):
"""
on wechat enterprise delete user event, only has UserID, no Other fields
:param xml_tree: ET.fromstring(str) instance
:param company: receive company
:return:
"""
we_id = xml_tree.find('UserID').text
employee = self.sudo().search([('we_id', '=', we_id), ('company_id', '=', company.id)])
if employee.id is not False:
employee.write({'active': False})
return 'success'
+11
View File
@@ -0,0 +1,11 @@
from odoo import models, fields
class WechatEnterpriseLog(models.Model):
_name = 'wechat.enterprise.log'
_description = 'Wechat Enterprise Log'
_order = 'create_date desc'
company_id = fields.Many2one('res.company', string='Company')
we_app_id = fields.Many2one('wechat.enterprise.app', string='Wechat Enterprise App')
detail = fields.Text(string='Detail')
+34
View File
@@ -0,0 +1,34 @@
from odoo import models
from odoo.exceptions import AccessDenied
from ..common.custom_encrypt import CustomEncrypt
class ResUsers(models.Model):
_inherit = 'res.users'
we_auth_secret = 'RZDeROZp'
def _check_credentials(self, password, env):
"""
if password is dict and the key of Non password authentication(npa) is True, then authentication by custom
:param password:
:param env:
:return:
"""
if isinstance(password, dict) and password.get('npa', False) is True:
if not self.authentication_by_non_password(password, env):
raise AccessDenied()
else:
super()._check_credentials(password, env)
def authentication_by_non_password(self, secret_dict, env):
"""
authentication by custom, can override this method to implement others authentication
:param secret_dict: {'npa': True, 'type': 'we', 'password': 'xxxx'}, can add more type to implement others
:param env:
:return:
"""
real_password = secret_dict.get('password', None)
if secret_dict.get('type', None) == 'we':
return CustomEncrypt.is_equal(ResUsers.we_auth_secret, real_password)
return False
@@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_wechat_enterprise_app,wechat.enterprise.app,model_wechat_enterprise_app,base.group_system,1,1,1,1
access_wechat_enterprise_log,wechat.enterprise.log,model_wechat_enterprise_log,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_wechat_enterprise_app wechat.enterprise.app model_wechat_enterprise_app base.group_system 1 1 1 1
3 access_wechat_enterprise_log wechat.enterprise.log model_wechat_enterprise_log base.group_system 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

@@ -0,0 +1,46 @@
/** @odoo-module */
import {registry} from "@web/core/registry";
const {Component, xml, useEffect, onWillStart} = owl;
import {loadJS} from "@web/core/assets";
import {useService} from "@web/core/utils/hooks";
class WEQrcode extends Component {
setup() {
this.rpc = useService("rpc");
onWillStart(() => loadJS("http://wwcdn.weixin.qq.com/node/wework/wwopen/js/wwLogin-1.2.7.js"));
useEffect(async () => {
// to safety, we can not request by search_read, so we use rpc
const app = await this.rpc('/we/oauth2/info', {
app_id: this.props.appId
})
new WwLogin({
"id": "wx_reg",
"appid": app.corp_id,
"agentid": app.agentid,
"redirect_uri": encodeURIComponent(app.redirect_uri),
});
}, () => [])
}
}
WEQrcode.template = xml`
<div style="width: 100%;height: 100%">
<div id="wx_reg" style="width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;"></div>
</div>
`;
WEQrcode.defaultProps = {
appId: "",
};
WEQrcode.extractProps = ({attrs}) => {
return {
appId: attrs.app_id,
};
};
registry.category("view_widgets").add("we_qrcode", WEQrcode);
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="wechat_enterprise_app_form" model="ir.ui.view">
<field name="name">wechat.enterprise.app.form</field>
<field name="model">wechat.enterprise.app</field>
<field name="arch" type="xml">
<form delete="0">
<group col="4">
<group colspan="2">
<field name="name"/>
<field name="corp_id"/>
<field name="sync_with_user"/>
</group>
<group colspan="2">
<field name="agentid"/>
<field name="secret" password="True"/>
</group>
<group colspan="4">
<field name="description"/>
</group>
<h2 style="margin-top: 4px;">Url Verify Settings</h2>
<group colspan="4">
<field name="verify_txt" filename="verify_txt_filename"/>
<filed name="verify_txt_filename" invisible="1"/>
</group>
</group>
</form>
</field>
</record>
</odoo>
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="we_company_extend" model="ir.ui.view">
<field name="name">we.company.extend</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="mode">primary</field>
<field name="priority">2</field>
<field name="arch" type="xml">
<xpath expr="//page[@name='general_info']" position="after">
<page string="Wechat Enterprise Apps" name="wechat_enterprise_apps">
<group>
<field name="we_corp_id"/>
</group>
<h2 style="margin-top: 4px;">Callback Settings</h2>
<group>
<field name="we_cb_token" password="True"/>
<field name="we_cb_encoding_AES_key" password="True"/>
</group>
<field name="app_ids">
<tree create="1" delete="1">
<field name="name"/>
<field name="agentid"/>
<field name="sync_with_user"/>
<!-- need this field, if not can not save filename -->
<field name="verify_txt_filename" invisible="1"/>
<field name="secret"/>
<button name="run_sync" string="Sync Organization" type="object"
icon="fa-refresh text-primary"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
</odoo>
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="wechat_enterprise_log_tree" model="ir.ui.view">
<field name="name">wechat.enterprise.log.tree</field>
<field name="model">wechat.enterprise.log</field>
<field name="arch" type="xml">
<tree edit="0" create="0" delete="0">
<field name="company_id"/>
<field name="we_app_id"/>
<field name="create_uid"/>
<field name="create_date"/>
<field name="detail"/>
</tree>
</field>
</record>
<record id="wechat_enterprise_log_form" model="ir.ui.view">
<field name="name">wechat.enterprise.log.form</field>
<field name="model">wechat.enterprise.log</field>
<field name="arch" type="xml">
<form edit="0" create="0" delete="0">
<group col="4">
<group colspan="2">
<field name="company_id" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="create_uid"/>
</group>
<group colspan="2">
<field name="we_app_id" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="create_date"/>
</group>
<group colspan="4">
<field name="detail"/>
</group>
</group>
</form>
</field>
</record>
<record id="wechat_enterprise_log_act_window" model="ir.actions.act_window">
<field name="name">Wechat Enterprise Logs</field>
<field name="res_model">wechat.enterprise.log</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="menu_wechat_enterprise_log"
name="Wechat Enterprise Logs"
parent="hr.menu_hr_root"
groups="base.group_system"
action="wechat_enterprise_log_act_window"
sequence="99"/>
</odoo>
+2 -2
View File
@@ -12,8 +12,8 @@ class IssueLibrary(models.Model):
name = fields.Char(string='议题名称')
code = fields.Char(string='编号', required=True, copy=False,
default=lambda self: self.env['ir.sequence'].next_by_code('issue_library_code'))
type_issue = fields.Selection([('business_reporting', '业务汇报'), ('project_progress', '项目进展'),
('hr', '人力资源'), ('financial_reporting', '财务报告')], string="议题类别")
type_issue = fields.Selection([('business_report', '业务汇报'), ('project_progress', '项目进展'),
('hr', '人力资源'), ('account_report', '财务报告')], string="议题类别")
objective = fields.Char(string='目的')
employee_ids = fields.Many2many('hr.employee', string='参与人员')
meeting_et = fields.Datetime(string='会议预计时间')