diff --git a/tianhe_approvals/data/approval_category_type_data.xml b/tianhe_approvals/data/approval_category_type_data.xml index 71c87042d..d8022159a 100644 --- a/tianhe_approvals/data/approval_category_type_data.xml +++ b/tianhe_approvals/data/approval_category_type_data.xml @@ -6,25 +6,15 @@ 采购办公用品 purchase_work - - - 党总支发文 - general_party_branch_post - 建设工程业务呈批表 construction_business - - - 董事会议题申报表 - tianhe_dsh_issues_declarations - 工资呈批表 - tianhe_dsh_issues_declarations + salary_approval @@ -32,11 +22,6 @@ management_declare 1 - - - 公文发文(子企业) - official_issues_for_son - 公文发文 @@ -67,11 +52,6 @@ 诉讼事项录入表 contentious_matter - - - 投融资发文 - financing_post - 业务呈批表 @@ -95,27 +75,27 @@ 补卡 - generic_forms_replacement_card + card_approval 加班 - generic_forms_overtime + overtime_approval 外出 - generic_forms_outing + go_approval 销假 - generic_forms_remove_holiday + remove_holiday_approval 离职表 - dimission + exit_form @@ -132,12 +112,6 @@ 调岗 generic_forms_reassignment - - - - 离职表 - dimission - 试用期转正式员工 @@ -148,11 +122,6 @@ 实习期转试用期 exercitation_tryout - - - 党支委发文 - party_branch_committee_post - 调班登记表 @@ -203,10 +172,10 @@ 业务呈批表(粮库专用) liangchu_business_approval_form_grain_depot - + - 总经理办公会议议题呈批表 - liangchu_general_manager_office_meeting_topics + 议题申报表 + issue_library_approval @@ -216,7 +185,7 @@ 请假登记单 - leave_registration + holiday_approval @@ -243,11 +212,6 @@ 跨部门协同 departmental_collaboration - - - 收文呈批 - receipt_submission - 费用调整申请表 @@ -258,22 +222,11 @@ 费用支出 fee_expend - - - 发文呈批 - post_submission - 督办工作表 supervision_worksheets - - - 公众号/云短信发文呈批 - official_account_cloud_sms_post_submission - 用章申请 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版本的企业微信模块 + +
+ +[![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) + +
+ +## ✨ 特点 + +- 采用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源码解读: + + + +更多模块或进群交流欢迎加微信: + + \ 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 + +
+ + + + + + + + + + + + + + + + +

Url Verify Settings

+ + + + + +
+
+
+
+
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

+ + + + + + + + + + + + + + +