用章申请
diff --git a/tianhe_approvals/models/inherit_approval_category.py b/tianhe_approvals/models/inherit_approval_category.py
index 7effbb557..3d98e8fe3 100644
--- a/tianhe_approvals/models/inherit_approval_category.py
+++ b/tianhe_approvals/models/inherit_approval_category.py
@@ -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', '租赁合同审批单'),
diff --git a/xy_wechat-master/.gitignore b/xy_wechat-master/.gitignore
new file mode 100644
index 000000000..3b516ebf3
--- /dev/null
+++ b/xy_wechat-master/.gitignore
@@ -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
diff --git a/xy_wechat-master/LICENSE.md b/xy_wechat-master/LICENSE.md
new file mode 100644
index 000000000..63b4b681c
--- /dev/null
+++ b/xy_wechat-master/LICENSE.md
@@ -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.
\ No newline at end of file
diff --git a/xy_wechat-master/README.md b/xy_wechat-master/README.md
new file mode 100644
index 000000000..3ea650c0a
--- /dev/null
+++ b/xy_wechat-master/README.md
@@ -0,0 +1,84 @@
+# xy_wechat
+适用于Odoo16版本的企业微信模块
+
+
+
+[](https://github.com/xunonxyz/xy_wechat/blob/master/LICENSE.md)
+[](https://github.com/xunonxyz/xy_wechat/issues)
+[](https://github.com/odoo/odoo/tree/16.0)
+
+
+
+## ✨ 特点
+
+- 采用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
+
+
+
+### 同步
+
+配置完参数后点击「同步组织架构」按钮即可。同步是在后端异步进行,不会阻塞当前操作,但尽量不要进行用户、部门的变更、创建、删除等操作。
+
+同步完成或失败后均会有通知窗进行通知。
+
+
+
+### 同步日志
+
+每次同步的结果、报错代码栈、花费的时间等信息均可以进行查看。
+
+在「员工 - 企业微信模块日志」页面可进行查看
+
+
+
+## ❤️ 支持
+
+富能通企业官网:https://www.funenc.com/
+
+免费后端培训视频系列:https://www.bilibili.com/video/BV1rP411G7CZ/
+
+知识星球Odoo源码解读:
+
+
+
+更多模块或进群交流欢迎加微信:
+
+
\ No newline at end of file
diff --git a/xy_wechat-master/__init__.py b/xy_wechat-master/__init__.py
new file mode 100644
index 000000000..511a0ca3a
--- /dev/null
+++ b/xy_wechat-master/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import controllers
+from . import models
\ No newline at end of file
diff --git a/xy_wechat-master/__manifest__.py b/xy_wechat-master/__manifest__.py
new file mode 100644
index 000000000..173b67072
--- /dev/null
+++ b/xy_wechat-master/__manifest__.py
@@ -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
+}
diff --git a/xy_wechat-master/common/__init__.py b/xy_wechat-master/common/__init__.py
new file mode 100644
index 000000000..2c24d6f3e
--- /dev/null
+++ b/xy_wechat-master/common/__init__.py
@@ -0,0 +1,4 @@
+from . import token_store
+from . import we_request
+from . import custom_encrypt
+from . import callback
diff --git a/xy_wechat-master/common/callback/WXBizMsgCrypt.py b/xy_wechat-master/common/callback/WXBizMsgCrypt.py
new file mode 100644
index 000000000..24a618962
--- /dev/null
+++ b/xy_wechat-master/common/callback/WXBizMsgCrypt.py
@@ -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 = """
+
+
+%(timestamp)s
+
+"""
+
+ 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
diff --git a/xy_wechat-master/common/callback/__init__.py b/xy_wechat-master/common/callback/__init__.py
new file mode 100644
index 000000000..c298ce9ec
--- /dev/null
+++ b/xy_wechat-master/common/callback/__init__.py
@@ -0,0 +1,2 @@
+from . import ierror
+from . import WXBizMsgCrypt
diff --git a/xy_wechat-master/common/callback/ierror.py b/xy_wechat-master/common/callback/ierror.py
new file mode 100644
index 000000000..6c7ca1221
--- /dev/null
+++ b/xy_wechat-master/common/callback/ierror.py
@@ -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
diff --git a/xy_wechat-master/common/custom_encrypt.py b/xy_wechat-master/common/custom_encrypt.py
new file mode 100644
index 000000000..3a84dcdf3
--- /dev/null
+++ b/xy_wechat-master/common/custom_encrypt.py
@@ -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
diff --git a/xy_wechat-master/common/token_store.py b/xy_wechat-master/common/token_store.py
new file mode 100644
index 000000000..754f34413
--- /dev/null
+++ b/xy_wechat-master/common/token_store.py
@@ -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)
diff --git a/xy_wechat-master/common/we_request.py b/xy_wechat-master/common/we_request.py
new file mode 100644
index 000000000..6cf85b598
--- /dev/null
+++ b/xy_wechat-master/common/we_request.py
@@ -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)
diff --git a/xy_wechat-master/controllers/__init__.py b/xy_wechat-master/controllers/__init__.py
new file mode 100644
index 000000000..457bae27e
--- /dev/null
+++ b/xy_wechat-master/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import controllers
\ No newline at end of file
diff --git a/xy_wechat-master/controllers/controllers.py b/xy_wechat-master/controllers/controllers.py
new file mode 100644
index 000000000..8e4674be2
--- /dev/null
+++ b/xy_wechat-master/controllers/controllers.py
@@ -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_.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/', 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//', 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 ''
diff --git a/xy_wechat-master/i18n/zh_CN.po b/xy_wechat-master/i18n/zh_CN.po
new file mode 100644
index 000000000..379be37ca
--- /dev/null
+++ b/xy_wechat-master/i18n/zh_CN.po
@@ -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 "网页授权验证文件"
diff --git a/xy_wechat-master/models/__init__.py b/xy_wechat-master/models/__init__.py
new file mode 100644
index 000000000..1348d3312
--- /dev/null
+++ b/xy_wechat-master/models/__init__.py
@@ -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
+
diff --git a/xy_wechat-master/models/app.py b/xy_wechat-master/models/app.py
new file mode 100644
index 000000000..677256a88
--- /dev/null
+++ b/xy_wechat-master/models/app.py
@@ -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()
diff --git a/xy_wechat-master/models/company.py b/xy_wechat-master/models/company.py
new file mode 100644
index 000000000..1a0351f18
--- /dev/null
+++ b/xy_wechat-master/models/company.py
@@ -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')
diff --git a/xy_wechat-master/models/department.py b/xy_wechat-master/models/department.py
new file mode 100644
index 000000000..f106bb31f
--- /dev/null
+++ b/xy_wechat-master/models/department.py
@@ -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'
diff --git a/xy_wechat-master/models/employee.py b/xy_wechat-master/models/employee.py
new file mode 100644
index 000000000..ffe49f641
--- /dev/null
+++ b/xy_wechat-master/models/employee.py
@@ -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'
diff --git a/xy_wechat-master/models/log.py b/xy_wechat-master/models/log.py
new file mode 100644
index 000000000..724a7a209
--- /dev/null
+++ b/xy_wechat-master/models/log.py
@@ -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')
diff --git a/xy_wechat-master/models/res_users.py b/xy_wechat-master/models/res_users.py
new file mode 100644
index 000000000..7aa67a2a9
--- /dev/null
+++ b/xy_wechat-master/models/res_users.py
@@ -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
diff --git a/xy_wechat-master/security/ir.model.access.csv b/xy_wechat-master/security/ir.model.access.csv
new file mode 100644
index 000000000..569899093
--- /dev/null
+++ b/xy_wechat-master/security/ir.model.access.csv
@@ -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
\ No newline at end of file
diff --git a/xy_wechat-master/static/description/icon.png b/xy_wechat-master/static/description/icon.png
new file mode 100644
index 000000000..394162386
Binary files /dev/null and b/xy_wechat-master/static/description/icon.png differ
diff --git a/xy_wechat-master/static/img/knowledge_star.png b/xy_wechat-master/static/img/knowledge_star.png
new file mode 100644
index 000000000..6523919d7
Binary files /dev/null and b/xy_wechat-master/static/img/knowledge_star.png differ
diff --git a/xy_wechat-master/static/img/log.png b/xy_wechat-master/static/img/log.png
new file mode 100644
index 000000000..7acd88081
Binary files /dev/null and b/xy_wechat-master/static/img/log.png differ
diff --git a/xy_wechat-master/static/img/notice.png b/xy_wechat-master/static/img/notice.png
new file mode 100644
index 000000000..3fb8b12e2
Binary files /dev/null and b/xy_wechat-master/static/img/notice.png differ
diff --git a/xy_wechat-master/static/img/setting.png b/xy_wechat-master/static/img/setting.png
new file mode 100644
index 000000000..b60c4b099
Binary files /dev/null and b/xy_wechat-master/static/img/setting.png differ
diff --git a/xy_wechat-master/static/img/wx_qrcode.png b/xy_wechat-master/static/img/wx_qrcode.png
new file mode 100644
index 000000000..98fd503f9
Binary files /dev/null and b/xy_wechat-master/static/img/wx_qrcode.png differ
diff --git a/xy_wechat-master/static/src/js/we_qrcode_widget.js b/xy_wechat-master/static/src/js/we_qrcode_widget.js
new file mode 100644
index 000000000..dd4638e7a
--- /dev/null
+++ b/xy_wechat-master/static/src/js/we_qrcode_widget.js
@@ -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`
+
+`;
+
+WEQrcode.defaultProps = {
+ appId: "",
+};
+WEQrcode.extractProps = ({attrs}) => {
+ return {
+ appId: attrs.app_id,
+ };
+};
+
+registry.category("view_widgets").add("we_qrcode", WEQrcode);
\ No newline at end of file
diff --git a/xy_wechat-master/views/app.xml b/xy_wechat-master/views/app.xml
new file mode 100644
index 000000000..c8e5c9bfc
--- /dev/null
+++ b/xy_wechat-master/views/app.xml
@@ -0,0 +1,34 @@
+
+
+
+ wechat.enterprise.app.form
+ wechat.enterprise.app
+
+
+
+
+
diff --git a/xy_wechat-master/views/company.xml b/xy_wechat-master/views/company.xml
new file mode 100644
index 000000000..66aae73ea
--- /dev/null
+++ b/xy_wechat-master/views/company.xml
@@ -0,0 +1,39 @@
+
+
+
+ we.company.extend
+ res.company
+
+ primary
+ 2
+
+
+
+
+
+
+
+ Callback Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/xy_wechat-master/views/log.xml b/xy_wechat-master/views/log.xml
new file mode 100644
index 000000000..8c3e52a8f
--- /dev/null
+++ b/xy_wechat-master/views/log.xml
@@ -0,0 +1,52 @@
+
+
+
+ wechat.enterprise.log.tree
+ wechat.enterprise.log
+
+
+
+
+
+
+
+
+
+
+
+
+ wechat.enterprise.log.form
+ wechat.enterprise.log
+
+
+
+
+
+
+ Wechat Enterprise Logs
+ wechat.enterprise.log
+ tree,form
+
+
+
+
diff --git a/yuthon_approval/models/issue_library.py b/yuthon_approval/models/issue_library.py
index 22657d8a8..961a188bb 100644
--- a/yuthon_approval/models/issue_library.py
+++ b/yuthon_approval/models/issue_library.py
@@ -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='会议预计时间')