Compare commits

...

34 Commits

Author SHA1 Message Date
胡尧
0082a308fa 修改屏蔽登录页footer 2024-09-23 16:37:36 +08:00
胡尧
5024a9254d 删除皮肤文件 2024-09-23 16:08:49 +08:00
胡尧
ae844cf203 Merge branch 'develop' into feature/wechat_message 2024-09-23 14:03:18 +08:00
胡尧
dd5c9775fc 去掉修改网页标题的代码 2024-09-23 11:18:40 +08:00
黄焱
82c274591c Accept Merge Request #1336: (feature/前端样式修改 -> develop)
Merge Request: 漏提交代码

Created By: @黄焱
Reviewed By: @马广威
Approved By: @马广威 
Accepted By: @黄焱
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1336?initial=true
2024-09-23 11:00:15 +08:00
hy
4f73f57ddf 漏提交代码 2024-09-23 10:59:10 +08:00
黄焱
269141dfb2 Accept Merge Request #1335: (feature/前端样式修改 -> develop)
Merge Request: 顶部菜单支持多级菜单

Created By: @黄焱
Reviewed By: @马广威
Approved By: @马广威 
Accepted By: @黄焱
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1335?initial=true
2024-09-23 10:56:37 +08:00
hy
cab0e1ce0b 顶部菜单支持多级菜单 2024-09-23 10:54:37 +08:00
禹翔辉
d4ff7ffaa9 Accept Merge Request #1334: (feature/产品优化 -> develop)
Merge Request: 产品字段值优化

Created By: @禹翔辉
Reviewed By: @马广威
Approved By: @马广威 
Accepted By: @禹翔辉
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1334?initial=true
2024-09-23 10:51:00 +08:00
yuxianghui
d238d09cc3 Merge branch 'feature/刀具优化' into feature/产品优化 2024-09-23 10:49:36 +08:00
yuxianghui
4cee5213bb 产品字段值优化 2024-09-23 10:48:48 +08:00
杨金灵
35fda7106a Accept Merge Request #1333: (feature/销售和排程添加消息推送 -> develop)
Merge Request: 销售和排程添加消息推送

Created By: @杨金灵
Reviewed By: @马广威
Approved By: @马广威 
Accepted By: @杨金灵
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1333
2024-09-23 10:32:05 +08:00
jinling.yang
b8baa84270 还原代码 2024-09-23 10:30:51 +08:00
jinling.yang
6591e663b6 修复待排程的消息推送 2024-09-23 10:26:53 +08:00
jinling.yang
bad5c8d489 Merge branch 'develop' of https://e.coding.net/jikimo-hn/jikimo_sfs/jikimo_sf into feature/销售和排程添加消息推送 2024-09-23 08:56:59 +08:00
jinling.yang
bbf62d2302 消息模块添加权限 2024-09-20 17:32:02 +08:00
jinling.yang
17b09c1f6d Merge branch 'feature/wechat_message' of https://e.coding.net/jikimo-hn/jikimo_sfs/jikimo_sf into feature/销售和排程添加消息推送 2024-09-20 16:57:19 +08:00
禹翔辉
e0fc70ec60 Accept Merge Request #1332: (feature/刀具优化 -> develop)
Merge Request: 1、刀具标准库优化,整体式刀具物料产品优化;刀具同步接口优化;2、新增组装单扫描确认组装和获取测量值功能;组装单更换物料弹窗信息优化;

Created By: @禹翔辉
Reviewed By: @马广威
Approved By: @马广威 
Accepted By: @禹翔辉
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1332?initial=true
2024-09-20 16:55:14 +08:00
yuxianghui
4558fc0336 Merge branch 'feature/库存优化' into feature/刀具优化 2024-09-20 16:51:39 +08:00
yuxianghui
4609ec442a 1、刀具标准库优化,整体式刀具物料产品优化;刀具同步接口优化;2、新增组装单扫描确认组装和获取测量值功能;组装单更换物料弹窗信息优化; 2024-09-20 16:50:55 +08:00
黄焱
85fea64f49 Accept Merge Request #1331: (feature/前端样式修改 -> develop)
Merge Request: 修改顶部下拉菜单

Created By: @黄焱
Reviewed By: @马广威
Approved By: @马广威 
Accepted By: @黄焱
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1331?initial=true
2024-09-20 16:31:57 +08:00
hy
f74215c9f6 修改顶部下拉菜单 2024-09-20 16:27:12 +08:00
jinling.yang
cc8906980c 销售和排程添加消息推送 2024-09-20 15:42:07 +08:00
马广威
0990d73075 Accept Merge Request #1329: (feature/制造功能优化 -> develop)
Merge Request: 处理提示词制造订单重复问题;处理获取数据按钮显隐问题

Created By: @马广威
Accepted By: @马广威
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1329?initial=true
2024-09-20 11:46:38 +08:00
mgw
7d877a0cbb 处理提示词制造订单重复问题;处理获取数据按钮显隐问题 2024-09-20 11:45:23 +08:00
胡尧
e13bad8483 Merge branch 'develop' into feature/wechat_message 2024-09-20 10:43:41 +08:00
胡尧
22ebb1bbe1 增加扩展 2024-09-20 10:43:13 +08:00
廖丹龙
bc888d7984 Accept Merge Request #1328: (feature/tax_sync -> develop)
Merge Request: 刀具岗权限调整

Created By: @廖丹龙
Reviewed By: @胡尧
Approved By: @胡尧 
Accepted By: @廖丹龙
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1328
2024-09-20 10:42:55 +08:00
胡尧
c92d4f7868 增加扩展 2024-09-20 10:41:41 +08:00
管欢
667a2a81fb Accept Merge Request #1327: (feature/org_info_synchronous -> develop)
Merge Request: 左侧图标修改

Created By: @管欢
Reviewed By: @胡尧
Approved By: @胡尧 
Accepted By: @管欢
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1327
2024-09-20 10:40:26 +08:00
guanhuan
d067c5b8c4 左侧图标修改 2024-09-20 09:49:13 +08:00
jinling.yang
f6e371f223 Merge branch 'feature/wechat_message' of https://e.coding.net/jikimo-hn/jikimo_sfs/jikimo_sf into feature/new 2024-09-20 09:25:00 +08:00
胡尧
0aefe9e656 修改目录结构 2024-09-20 09:19:34 +08:00
胡尧
4f8f29e41a 新增消息通知模块 2024-09-19 17:56:00 +08:00
321 changed files with 297 additions and 18233 deletions

View File

@@ -5,9 +5,9 @@
<!-- 修改页面头部图标及文字 -->
<template id="favicon_icon" inherit_id="web.layout" name="Web layout">
<!-- change the title with reliance partner -->
<xpath expr="//head//title" position="before">
<!-- <xpath expr="//head//title" position="before">
<title t-esc="'JIKIMO'"/>
</xpath>
</xpath> -->
<!-- change the default favicon icon with -->
<xpath expr="//head//link[@rel='shortcut icon']" position="replace">
<link type="image/x-icon" rel="shortcut icon" href="/jikimo_frontend/static/src/img/jikimo-logo.ico"/>
@@ -16,7 +16,7 @@
<!-- hide 登录页面 powerd by odoo 及管理数据库 -->
<template id="login_page_layout" inherit_id="web.login_layout" name="Login Page Layout">
<xpath expr="//div[@class='card-body']//div[last()]" position="replace"></xpath>
<xpath expr="//div[@class='card-body']/div[last()]" position="replace"></xpath>
</template>
<!-- 隐藏odoo版本信息 -->

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': '机企猫智能工厂 消息提醒',
'version': '1.0',
'summary': '智能工厂消息提醒模块',
'sequence': 1,
'description': """
""",
'category': 'sf',
'website': 'https://www.sf.jikimo.com',
'depends': ['jikimo_message_notify'],
'data': [
'security/ir.model.access.csv',
'data/bussiness_node.xml',
# 'views/sf_message_template_view.xml',
],
'test': [
],
'license': 'LGPL-3',
'installable': True,
'auto_install': False,
'application': False,
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<odoo>
<data>
<record id="bussiness_1" model="jikimo.message.bussiness.node">
<field name="name">订单确认</field>
<field name="model">sale.order</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,2 @@
from . import jikimo_message_template
from . import sale_order

View File

@@ -0,0 +1,10 @@
from odoo import models, fields, api
class JikimoMessageTemplate(models.Model):
_inherit = "jikimo.message.template"
def _get_message_model(self):
res = super(JikimoMessageTemplate, self)._get_message_model()
res.append("sale.order")
return res

View File

@@ -0,0 +1,12 @@
from odoo import models, fields, api
class SaleOrder(models.Model):
_name = "sale.order"
_description = "销售订单"
_inherit = ["sale.order", "jikimo.message.dispatch"]
def create(self, vals_list):
res = super(SaleOrder, self).create(vals_list)
res.add_queue('订单确认')
return res

View File

@@ -0,0 +1,5 @@
<odoo>
<data>
</data>
</odoo>

View File

@@ -1,2 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_theme_data,access.theme.data,model_theme_data,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2
3
4
5
6

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © <2016> <top hy>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<odoo>
<data>
<record id="sf_message_template_view_form" model="ir.ui.view">
<field name="name">sf.message.template.view.form</field>
<field name="model">message.template</field>
<field name="arch" type="xml">
<form string="消息模板">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" class="w-100" required="1"/>
</h1>
</div>
<group>
<!-- <field name="type"/>-->
<field name="notify_model_id"/>
<field name="content" widget="html" class="oe-bordered-editor"
options="{'style-inline': true, 'codeview': true, 'dynamic_placeholder': true}"/>
<field name="description"/>
<field name="msgtype"/>
<field name="notification_department_id"/>
<field name="notification_employee_ids" widget="many2many_tags"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="sf_message_template_view_tree" model="ir.ui.view">
<field name="name">sf.message.template.view.tree</field>
<field name="model">message.template</field>
<field name="arch" type="xml">
<tree string="消息模板">
<field name="name"/>
<!-- <field name="type"/>-->
<field name="content"/>
<field name="msgtype"/>
<field name="notification_department_id"/>
<field name="notification_employee_ids" widget="many2many_tags"/>
<field name="description"/>
</tree>
</field>
</record>
<record id="sf_message_template_search_view" model="ir.ui.view">
<field name="name">sf.message.template.search.view</field>
<field name="model">message.template</field>
<field name="arch" type="xml">
<search>
<field name="name" string="模糊搜索"
filter_domain="['|','|',('name','like',self),('description','like',self)]"/>
<field name="name"/>
<filter name="filter_active" string="已归档" domain="[('active','=',False)]"/>
</search>
</field>
</record>
<!--定义单证类型视图动作-->
<record id="sf_message_template_action" model="ir.actions.act_window">
<field name="name">消息模板</field>
<field name="res_model">message.template</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="sf_message_template_view_tree"/>
</record>
<menuitem id="msg_set_menu" name="消息设置" parent="base.menu_administration" sequence="1"/>
<menuitem id="sf_message_template_send_menu" name="消息模板" parent="msg_set_menu"
action="sf_message_template_action" sequence="1"/>
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -234,7 +234,7 @@
</record>
<menuitem name="系统工单" id="work_order_1_list" web_icon="jikimo_system_order,static/description/icon.png"/>
<menuitem name="系统工单" id="work_order_1_list" web_icon="jikimo_system_order,static/description/系统工单.png"/>
<menuitem name="工单" id="work_order" parent="work_order_1_list" action="system_order"/>
<menuitem name="工单模板" id="work_order_template" parent="work_order_1_list" action="work_template" groups="jikimo_system_order.group_operations_permissions_rwc"/>
<menuitem name="工单分类" id="work_order_type" parent="work_order_1_list" action="classify" groups="jikimo_system_order.group_operations_permissions_rwc"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

View File

@@ -1024,7 +1024,7 @@
<menuitem
id="menu_quality_root"
name="Quality"
web_icon="quality_control,static/description/icon.svg"
web_icon="quality_control,static/description/质量.png"
sequence="150"
groups="quality.group_quality_user"/>

View File

@@ -1,2 +1,3 @@
pystrich
cpca
pycryptodome==3.20

View File

@@ -21,8 +21,9 @@ class ToolMaterialsBasicParameters(models.Model):
neck_length = fields.Float('颈部长度(mm)')
handle_diameter = fields.Float('柄部直径(mm)')
handle_length = fields.Float('柄部长度(mm)')
blade_tip_diameter = fields.Integer('刀尖直径(mm)')
blade_tip_working_size = fields.Char('刀尖处理尺寸(R半径mm/倒角度)', size=20)
blade_tip_diameter = fields.Float('刀尖直径(mm)')
blade_tip_working_size = fields.Char('刀尖倒角度)', size=20)
tip_r_size = fields.Float('刀尖R角(mm)')
blade_tip_taper = fields.Integer('刀尖锥度(°)')
blade_diameter = fields.Float('刃部直径(mm)')
blade_length = fields.Float('刃部长度(mm)')

View File

@@ -30,6 +30,7 @@ patch(barcodeGenericHandlers, "start", {
"O-CMD.PAGER-FIRST": () => updatePager("first"),
"O-CMD.PAGER-LAST": () => updatePager("last"),
"O-CMD.CONFIRM": () => customClickOnButton(".jikimo_button_confirm"),
"O-CMD.FLUSHED": () => customClickOnButton(".jikimo_button_flushed"),
};
barcode.bus.addEventListener("barcode_scanned", (ev) => {

View File

@@ -15,6 +15,7 @@
<field name="handle_length"/>
<field name="blade_tip_diameter"/>
<field name="blade_tip_working_size"/>
<field name="tip_r_size"/>
<field name="blade_tip_taper"/>
<field name="blade_diameter"/>
<field name="blade_length"/>
@@ -95,6 +96,7 @@
<field name="handle_length"/>
<field name="blade_tip_diameter"/>
<field name="blade_tip_working_size"/>
<field name="tip_r_size"/>
<field name="blade_tip_taper"/>
<field name="blade_diameter"/>
<field name="blade_length"/>
@@ -139,6 +141,7 @@
</group>
<group attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}">
<field name="blade_tip_working_size"/>
<field name="tip_r_size"/>
<field name="blade_tip_diameter" class="diameter"/>
<field name="blade_tip_taper"/>
<field name="blade_helix_angle"/>

View File

@@ -222,6 +222,7 @@
<field name="handle_diameter" class="diameter"/>
<field name="handle_length"/>
<field name="blade_tip_working_size" class="du"/>
<field name="tip_r_size"/>
<field name="blade_tip_diameter" class="diameter"/>
<field name="blade_tip_taper" class="du"/>
<field name="blade_helix_angle" class="du"/>
@@ -577,7 +578,7 @@
</field>
</record>
<record model="ir.ui.view" id="view_cutting_tool_material_search">
<record model="ir.ui.view" id="view_cutting_tool_inventory_search">
<field name="name">sf.tool.inventory.search</field>
<field name="model">sf.tool.inventory</field>
<field name="arch" type="xml">

View File

@@ -191,6 +191,8 @@
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')],'readonly': [('id', '!=', False)]}"/>
<field name="cutting_tool_blade_tip_working_size"
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')],'readonly': [('id', '!=', False)]}"/>
<field name="cutting_tool_blade_tip_r_size"
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')],'readonly': [('id', '!=', False)]}"/>
<field name="cutting_tool_blade_tip_diameter" string="刀尖直径(mm)" class="diameter"
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')],'readonly': [('id', '!=', False)]}"/>
<field name="cutting_tool_blade_tip_taper" string="刀尖锥度(°)"

View File

@@ -623,7 +623,7 @@ class Sf_Dashboard_Connect(http.Controller):
# 未完成订单
not_done_orders = plan_obj.search(
[('production_line_id.name', '=', line), ('state', 'not in', ['finished']),
('production_id.state', 'not in', ['cancel']), ('active', '=', True)
('production_id.state', 'not in', ['cancel', 'done']), ('active', '=', True)
])
# print(not_done_orders)
# 完成订单

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

View File

@@ -1230,5 +1230,9 @@
action="hr_equipment_action1"
sequence="0"/>
<menuitem
id="maintenance.menu_maintenance_title"
web_icon="sf_maintenance,static/description/维护.png"/>
</odoo>

View File

@@ -1582,8 +1582,11 @@ class SfWorkOrderBarcodes(models.Model):
[('routing_type', '=', '装夹预调'), ('rfid_code', '=', barcode)])
if workorder_olds:
name = ''
tem_list = []
for workorder in workorder_olds:
name = '%s %s' % (name, workorder.production_id.name)
tem_list.append(workorder.production_id.name)
for i in list(set(tem_list)):
name = '%s %s' % (name, i)
raise UserError('该托盘已绑定【%s】制造订单,请先解除绑定!!!' % name)
if workorder:
if workorder.routing_type == '装夹预调':

View File

@@ -90,7 +90,8 @@ class ResProductMo(models.Model):
cutting_tool_coarse_medium_fine = fields.Selection([('', ''), ('', ''), ('', '')], '粗/中/精')
cutting_tool_run_out_accuracy_max = fields.Float('端跳精度max', digits=(6, 1))
cutting_tool_run_out_accuracy_min = fields.Float('端跳精度min', digits=(6, 1))
cutting_tool_blade_tip_working_size = fields.Char('刀尖处理尺寸(R半径mm/倒角)', size=20)
cutting_tool_blade_tip_working_size = fields.Char('刀尖倒角度(°)', size=20)
cutting_tool_blade_tip_r_size = fields.Float('刀尖R角(mm)')
fit_blade_shape_id = fields.Many2one('maintenance.equipment.image',
'适配刀片形状', domain=[('type', '=', '刀片形状')])
suitable_machining_method_ids = fields.Many2many('maintenance.equipment.image',
@@ -237,6 +238,7 @@ class ResProductMo(models.Model):
self.cutting_tool_blade_tip_taper = self.specification_id.blade_tip_taper
self.cutting_tool_blade_helix_angle = self.specification_id.blade_helix_angle
self.cutting_tool_blade_tip_working_size = self.specification_id.blade_tip_working_size
self.cutting_tool_blade_tip_r_size = self.specification_id.tip_r_size
self.cutting_tool_pitch = self.specification_id.pitch
self.cutting_tool_blade_width = self.specification_id.blade_width
self.cutting_tool_blade_depth = self.specification_id.blade_depth

View File

@@ -520,7 +520,7 @@
<xpath expr="//form//header" position="inside">
<button type="object" class="oe_highlight jikimo_button_confirm" name="get_three_check_datas"
string="获取数据" attrs='{"invisible": [("state","!=","progress")]}'/>
string="获取数据" attrs='{"invisible": [("state","!=","progress"), ("routing_type","!=","装夹预调")]}'/>
</xpath>

View File

@@ -11,10 +11,9 @@
""",
'category': 'sf',
'website': 'https://www.sf.jikimo.com',
'depends': ['base', 'sf_plan', 'sf_sale'],
'depends': ['sale', 'purchase', 'sf_plan', 'jikimo_message_notify'],
'data': [
'security/ir.model.access.csv',
'views/sf_message_template_view.xml',
'data/bussiness_node.xml'
],
'test': [
],

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" ?>
<odoo>
<data noupdate="1">
<record id="bussiness_pending_order" model="jikimo.message.bussiness.node">
<field name="name">待接单</field>
<field name="model">sale.order</field>
</record>
<record id="bussiness_to_be_confirm" model="jikimo.message.bussiness.node">
<field name="name">确认接单</field>
<field name="model">sale.order</field>
</record>
</data>
</odoo>

View File

@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
class SFMessageCamProgram(models.Model):
_name = 'sf.cam.work.order.program.knife.plan'
_inherit = ['sf.cam.work.order.program.knife.plan', 'sf.message.template']
_inherit = ['sf.cam.work.order.program.knife.plan', 'jikimo.message.dispatch']

View File

@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
class SFMessagefunctionalToolAssembly(models.Model):
_name = 'sf.functional.tool.assembly'
_inherit = ['sf.functional.tool.assembly', 'sf.message.template']
_inherit = ['sf.functional.tool.assembly', 'jikimo.message.dispatch']

View File

@@ -1,6 +1,25 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class SFMessagePlan(models.Model):
_name = 'sf.production.plan'
_inherit = ['sf.production.plan', 'sf.message.template']
_inherit = ['sf.production.plan', 'jikimo.message.dispatch']
# def create(self, vals_list):
# res = super(SFMessagePlan, self).create(vals_list)
# if res:
# try:
# res.add_queue('待排程')
# except Exception as e:
# logging.info('add_queue error:%s' % e)
# return res
#
# def _get_message(self):
# res = super(SFMessagePlan, self)._get_message()
# if res:
# try:
# res.add_queue('待排程')
# except Exception as e:
# logging.info('_get_message error:%s' % e)
# return res

View File

@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
class SFMessagePurchase(models.Model):
_name = 'purchase.order'
_inherit = ['purchase.order', 'sf.message.template']
_inherit = ['purchase.order', 'jikimo.message.dispatch']

View File

@@ -1,11 +1,39 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class SFMessageSale(models.Model):
_name = 'sale.order'
_inherit = ['sale.order', 'sf.message.template']
_inherit = ['sale.order', 'jikimo.message.dispatch']
# def create(self):
# res = super(SFMessageSale, self).create()
# if res is True:
def create(self, vals_list):
res = super(SFMessageSale, self).create(vals_list)
if res:
try:
res.add_queue('待接单')
except Exception as e:
logging.info('add_queue error:%s' % e)
return res
# 确认接单
def action_confirm(self):
res = super(SFMessageSale, self).action_confirm()
if res is True:
try:
self.add_queue('确认接单')
except Exception as e:
logging.info('add_queue error:%s' % e)
return res
# 继承并重写jikimo.message.dispatch的_get_message()
def _get_message(self, message_queue_ids):
res = super(SFMessageSale, self)._get_message(message_queue_ids)
if message_queue_ids.message_template_id.bussiness_node_id.name == '确认接单':
# sale_order = self.env['sale.order'].search([('id', '=', message_queue_ids.model.res_id)])
sale_order_line = self.env['sale.order.line'].search([('order_id', '=', int(message_queue_ids.res_id))])
if len(sale_order_line) == 1:
product = sale_order_line[0].product_id.name
elif len(sale_order_line) > 1:
product = '%s...' % sale_order_line[0].product_id.name
res[0] = res[0].replace('{{product_id}}', product)
return res

View File

@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
class SFMessageStockPicking(models.Model):
_name = 'stock.picking'
_inherit = ['stock.picking', 'sf.message.template']
_inherit = ['stock.picking', 'jikimo.message.dispatch']

View File

@@ -4,31 +4,9 @@ from abc import ABC, abstractmethod
class SfMessageTemplate(models.Model):
_name = "sf.message.template"
_description = u'消息模板'
_inherit = "jikimo.message.template"
name = fields.Char(string=u"名称", required=True)
description = fields.Char(string=u"描述")
content = fields.Html(string=u"内容", render_engine='qweb', translate=True, prefetch=True, sanitize=False)
msgtype = fields.Selection(
[('text', u'文字'), ('markdown', u'Markdown')], u'消息类型',
required=True, default='markdown')
notification_department_id = fields.Many2one('hr.department', u'通知部门', required=True)
notification_employee_ids = fields.Many2many('hr.employee', string=u'员工',
domain="[('department_id', '=',notification_department_id)]",
required=True)
is_send_time = fields.Boolean(string=u"定时发送", default=False)
send_time_1 = fields.Integer('发送时间点1')
send_time_2 = fields.Integer('发送时间点2')
active = fields.Boolean(string=u"是否有效", default=True)
@api.onchange('notification_department_id')
def _clear_employee_ids(self):
if self.notification_department_id:
self.notification_employee_ids = False
@abstractmethod
def dispatch(self, args):
"""
强迫继承该类必走该抽象方法'
"""
def _get_message_model(self):
res = super(SfMessageTemplate, self)._get_message_model()
res.append("sale.order")
return res

View File

@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
class SFMessageWork(models.Model):
_name = 'mrp.workorder'
_inherit = ['mrp.workorder', 'sf.message.template']
_inherit = ['mrp.workorder', 'jikimo.message.dispatch']

View File

@@ -1,9 +1,23 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sf_message_template_group_sale_salemanager,sf_message_template,model_sf_message_template,sf_base.group_sale_salemanager,1,1,1,0
access_sf_message_template_group_purchase,sf_message_template,model_sf_message_template,sf_base.group_purchase,1,1,1,0
access_sf_message_template_group_sf_stock_user,sf_message_template,model_sf_message_template,sf_base.group_sf_stock_user,1,1,1,0
access_sf_message_template_group_sf_order_user,sf_message_template,model_sf_message_template,sf_base.group_sf_order_user,1,1,1,0
access_sf_message_template_group_sf_tool_user,sf_message_template,model_sf_message_template,sf_base.group_sf_tool_user,1,1,1,0
access_jikimo_message_template_group_sale_salemanager,jikimo_message_template,model_jikimo_message_template,sf_base.group_sale_salemanager,1,1,1,0
access_jikimo_message_template_group_purchase,jikimo_message_template,model_jikimo_message_template,sf_base.group_purchase,1,1,1,0
access_jikimo_message_template_group_sf_stock_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_stock_user,1,1,1,0
access_jikimo_message_template_group_sf_order_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_order_user,1,1,1,0
access_jikimo_message_template_group_sf_tool_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_tool_user,1,1,1,0
access_jikimo_message_bussiness_node_group_sale_salemanager,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sale_salemanager,1,1,1,0
access_jikimo_message_bussiness_node_group_purchase,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_purchase,1,1,1,0
access_jikimo_message_bussiness_node_group_sf_stock_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_stock_user,1,1,1,0
access_jikimo_message_bussiness_node_group_sf_order_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_order_user,1,1,1,0
access_jikimo_message_bussiness_node_group_sf_tool_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_tool_user,1,1,1,0
access_jikimo_message_queue_group_sale_salemanager,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sale_salemanager,1,1,1,0
access_jikimo_message_queue_group_purchase,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_purchase,1,1,1,0
access_jikimo_message_queue_group_sf_stock_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_stock_user,1,1,1,0
access_jikimo_message_queue_group_sf_order_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_order_user,1,1,1,0
access_jikimo_message_queue_group_sf_tool_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_tool_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sf_message_template_group_sale_salemanager access_jikimo_message_template_group_sale_salemanager sf_message_template jikimo_message_template model_sf_message_template model_jikimo_message_template sf_base.group_sale_salemanager 1 1 1 0
3 access_sf_message_template_group_purchase access_jikimo_message_template_group_purchase sf_message_template jikimo_message_template model_sf_message_template model_jikimo_message_template sf_base.group_purchase 1 1 1 0
4 access_sf_message_template_group_sf_stock_user access_jikimo_message_template_group_sf_stock_user sf_message_template jikimo_message_template model_sf_message_template model_jikimo_message_template sf_base.group_sf_stock_user 1 1 1 0
5 access_sf_message_template_group_sf_order_user access_jikimo_message_template_group_sf_order_user sf_message_template jikimo_message_template model_sf_message_template model_jikimo_message_template sf_base.group_sf_order_user 1 1 1 0
6 access_sf_message_template_group_sf_tool_user access_jikimo_message_template_group_sf_tool_user sf_message_template jikimo_message_template model_sf_message_template model_jikimo_message_template sf_base.group_sf_tool_user 1 1 1 0
7 access_jikimo_message_bussiness_node_group_sale_salemanager jikimo_message_bussiness_node model_jikimo_message_bussiness_node sf_base.group_sale_salemanager 1 1 1 0
8 access_jikimo_message_bussiness_node_group_purchase jikimo_message_bussiness_node model_jikimo_message_bussiness_node sf_base.group_purchase 1 1 1 0
9 access_jikimo_message_bussiness_node_group_sf_stock_user jikimo_message_bussiness_node model_jikimo_message_bussiness_node sf_base.group_sf_stock_user 1 1 1 0
10 access_jikimo_message_bussiness_node_group_sf_order_user jikimo_message_bussiness_node model_jikimo_message_bussiness_node sf_base.group_sf_order_user 1 1 1 0
11 access_jikimo_message_bussiness_node_group_sf_tool_user jikimo_message_bussiness_node model_jikimo_message_bussiness_node sf_base.group_sf_tool_user 1 1 1 0
12 access_jikimo_message_queue_group_sale_salemanager jikimo_message_queue model_jikimo_message_queue sf_base.group_sale_salemanager 1 1 1 0
13 access_jikimo_message_queue_group_purchase jikimo_message_queue model_jikimo_message_queue sf_base.group_purchase 1 1 1 0
14 access_jikimo_message_queue_group_sf_stock_user jikimo_message_queue model_jikimo_message_queue sf_base.group_sf_stock_user 1 1 1 0
15 access_jikimo_message_queue_group_sf_order_user jikimo_message_queue model_jikimo_message_queue sf_base.group_sf_order_user 1 1 1 0
16 access_jikimo_message_queue_group_sf_tool_user jikimo_message_queue model_jikimo_message_queue sf_base.group_sf_tool_user 1 1 1 0
17
18
19
20
21
22
23

View File

@@ -7,7 +7,7 @@
<record id="sf_message_template_view_form" model="ir.ui.view">
<field name="name">sf.message.template.view.form</field>
<field name="model">sf.message.template</field>
<field name="model">message.template</field>
<field name="arch" type="xml">
<form string="消息模板">
<sheet>
@@ -18,14 +18,12 @@
</h1>
</div>
<group>
<!-- <field name="type"/>-->
<!-- <field name="type"/>-->
<field name="notify_model_id"/>
<field name="content" widget="html" class="oe-bordered-editor"
options="{'style-inline': true, 'codeview': true, 'dynamic_placeholder': true}"/>
<field name="description"/>
<field name="msgtype"/>
<field name="is_send_time"/>
<field name="send_time_1" attrs="{'invisible': [('is_send_time', '=', False)]}"/>
<field name="send_time_2" attrs="{'invisible': [('is_send_time', '=', False)]}"/>
<field name="notification_department_id"/>
<field name="notification_employee_ids" widget="many2many_tags"/>
</group>
@@ -36,16 +34,13 @@
<record id="sf_message_template_view_tree" model="ir.ui.view">
<field name="name">sf.message.template.view.tree</field>
<field name="model">sf.message.template</field>
<field name="model">message.template</field>
<field name="arch" type="xml">
<tree string="消息模板">
<field name="name"/>
<!-- <field name="type"/>-->
<!-- <field name="type"/>-->
<field name="content"/>
<field name="msgtype"/>
<field name="is_send_time"/>
<field name="send_time_1" attrs="{'invisible': [('is_send_time', '=', False)]}"/>
<field name="send_time_2" attrs="{'invisible': [('is_send_time', '=', False)]}"/>
<field name="notification_department_id"/>
<field name="notification_employee_ids" widget="many2many_tags"/>
<field name="description"/>
@@ -55,7 +50,7 @@
<record id="sf_message_template_search_view" model="ir.ui.view">
<field name="name">sf.message.template.search.view</field>
<field name="model">sf.message.template</field>
<field name="model">message.template</field>
<field name="arch" type="xml">
<search>
<field name="name" string="模糊搜索"
@@ -69,7 +64,7 @@
<!--定义单证类型视图动作-->
<record id="sf_message_template_action" model="ir.actions.act_window">
<field name="name">消息模板</field>
<field name="res_model">sf.message.template</field>
<field name="res_model">message.template</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="sf_message_template_view_tree"/>
</record>

View File

@@ -2438,6 +2438,7 @@ class CuttingToolBasicParameters(models.Model):
'handle_length': integral_tool_item['shank_length'],
'blade_tip_diameter': integral_tool_item['tip_diameter'],
'blade_tip_working_size': integral_tool_item['tip_handling_size'],
'tip_r_size': integral_tool_item['tip_r_size'],
'blade_tip_taper': integral_tool_item['knife_tip_taper'],
'blade_helix_angle': integral_tool_item['blade_helix_angle'],
'blade_width': integral_tool_item['blade_width'],
@@ -2459,6 +2460,7 @@ class CuttingToolBasicParameters(models.Model):
'handle_length': integral_tool_item['shank_length'],
'blade_tip_diameter': integral_tool_item['tip_diameter'],
'blade_tip_working_size': integral_tool_item['tip_handling_size'],
'tip_r_size': integral_tool_item['tip_r_size'],
'blade_tip_taper': integral_tool_item['knife_tip_taper'],
'blade_helix_angle': integral_tool_item['blade_helix_angle'],
'blade_width': integral_tool_item['blade_width'],
@@ -2789,6 +2791,7 @@ class CuttingToolBasicParameters(models.Model):
'handle_length': integral_tool_item['shank_length'],
'blade_tip_diameter': integral_tool_item['tip_diameter'],
'blade_tip_working_size': integral_tool_item['tip_handling_size'],
'tip_r_size': integral_tool_item['tip_r_size'],
'blade_tip_taper': integral_tool_item['knife_tip_taper'],
'blade_helix_angle': integral_tool_item['blade_helix_angle'],
'blade_width': integral_tool_item['blade_width'],
@@ -2810,6 +2813,7 @@ class CuttingToolBasicParameters(models.Model):
'handle_length': integral_tool_item['shank_length'],
'blade_tip_diameter': integral_tool_item['tip_diameter'],
'blade_tip_working_size': integral_tool_item['tip_handling_size'],
'tip_r_size': integral_tool_item['tip_r_size'],
'blade_tip_taper': integral_tool_item['knife_tip_taper'],
'blade_helix_angle': integral_tool_item['blade_helix_angle'],
'blade_width': integral_tool_item['blade_width'],

View File

@@ -357,6 +357,8 @@ class FunctionalToolAssembly(models.Model):
"""
智能工厂组装单处扫码校验刀具物料
"""
if 'O-CMD' in barcode:
return ''
for record in self:
tool_assembly_id = self.env['sf.functional.tool.assembly'].browse(self.ids)
lot_ids = self.env['stock.lot'].sudo().search([('rfid', '=', barcode)])
@@ -835,7 +837,7 @@ class FunctionalToolAssembly(models.Model):
if options == '刀柄+整体式刀具':
if not integral_ids:
raise ValidationError('功能刀具清单的BOM未配置[刀柄]信息请先配置BOM再开始组装')
raise ValidationError('功能刀具清单的BOM未配置[整体式刀具]信息请先配置BOM再开始组装')
return {'options': options, 'handle_ids': handle_ids, 'integral_ids': integral_ids}
elif options == '刀柄+刀杆+刀片':
if not blade_ids:

View File

@@ -298,8 +298,8 @@ class SfShelfLocationLot(models.Model):
brand_id = fields.Many2one('sf.machine.brand', '品牌', related='product_id.brand_id')
cutting_tool_blade_diameter = fields.Float('刃部直径(mm)', related='product_id.cutting_tool_blade_diameter')
cutting_tool_blade_tip_working_size = fields.Char('刀尖R角(mm)',
related='product_id.cutting_tool_blade_tip_working_size')
cutting_tool_blade_tip_working_size = fields.Float('刀尖R角(mm)',
related='product_id.cutting_tool_blade_tip_r_size')
cutting_tool_blade_radius = fields.Char('刀尖圆弧半径(mm)',
related='product_id.cutting_tool_blade_tip_circular_arc_radius')
cutting_tool_cutter_arbor_diameter = fields.Float('刀杆直径(mm)',

View File

@@ -18,8 +18,7 @@
<field name="arch" type="xml">
<tree create="0" export_xlsx="0" delete="0">
<header>
<button string="确认" name="set_tool_material" type="object"
class="treeHeaderBtn"/>
<button string="确认" name="set_tool_material" type="object" class="treeHeaderBtn"/>
</header>
<field name="name"/>
<field name="cutting_tool_type_id"/>
@@ -62,6 +61,7 @@
<field name="brand_id"/>
<field name="shelf_location_id"/>
<field name="lot_id"/>
<field name="qty"/>
</tree>
</field>
</record>
@@ -86,6 +86,7 @@
<field name="brand_id"/>
<field name="shelf_location_id"/>
<field name="lot_id"/>
<field name="qty"/>
</tree>
</field>
</record>
@@ -111,6 +112,7 @@
<field name="brand_id"/>
<field name="shelf_location_id"/>
<field name="lot_id"/>
<field name="qty"/>
</tree>
</field>
</record>
@@ -135,6 +137,7 @@
<field name="brand_id"/>
<field name="shelf_location_id"/>
<field name="lot_id"/>
<field name="qty"/>
</tree>
</field>
</record>

View File

@@ -478,9 +478,9 @@
class="btn-primary"/>
<button string="确认组装" name="functional_tool_assembly" type="object"
attrs="{'invisible': [('assemble_status', 'not in', ['01'])]}"
class="btn-primary"/>
class="btn-primary jikimo_button_confirm"/>
<button name="get_tool_preset_parameter" string="获取测量值"
type="object" class="btn-primary"
type="object" class="btn-primary jikimo_button_flushed"
attrs="{'invisible': [('assemble_status', 'in', ['0','1','2'])]}"
/>
<field name="assemble_status" widget="statusbar" statusbar_visible="0,01,1"/>

View File

@@ -16,7 +16,7 @@
<record id="view_tool_inventory_inherit_search" model="ir.ui.view">
<field name="name">sf.tool.inventory.inherit.search</field>
<field name="model">sf.tool.inventory</field>
<field name="inherit_id" ref="sf_base.view_cutting_tool_material_search"/>
<field name="inherit_id" ref="sf_base.view_cutting_tool_inventory_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='extension']" position="after">
<searchpanel>

View File

@@ -1,138 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
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/

View File

@@ -1,3 +0,0 @@
from . import we_api
from . import models
from . import controllers

View File

@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': '企业微信',
'version': '0.1',
'summary': '企业通讯录\消息处理\企业应用\无缝登录',
'sequence': 30,
"author": 'SmartGo Studio.,',
'description': '''用于企业内部员工的管理,
ER企业微信模块
=====================================================
主要针对odoo使用微信进行管理包括以下功能:
1) 公众号信息管理企业号下多applicaiton管理
2) 接收消息处理
3) 发送消息处理
4) 自定义菜单处理
....
本安装包使用了WechatEnterpriseSDK/wechat_sdk.py在此表示感谢。
源代码可以访问github.地址如下https://github.com/facert/WechatEnterpriseSDK
''',
'category': '基础信息',
'website': 'https://www.smartgo.cn',
'depends': ['base', 'mail','hr'],
'data': [
'security/ir.model.access.csv',
'views/we_config_view.xml',
'views/we_app_view.xml',
'views/we_send_message_view.xml',
'views/we_receive_message_view.xml',
'views/we_message_process_view.xml',
'views/we_templates.xml',
# "views/mail_view.xml",
"views/res_users_view.xml",
'views/menu_view.xml',
# 'views/we_menu.xml',
"data/data.xml"
],
'demo': [
'demo/we_config_demo.xml',
],
'qweb': [
# "static/src/xml/base.xml",
# "static/src/xml/account_payment.xml",
# "static/src/xml/account_report_backend.xml",
],
'installable': True,
'application': True,
'auto_install': False,
# 'post_init_hook': '_auto_install_l10n',
}

View File

@@ -1,273 +0,0 @@
#!/usr/bin/env python
# -*- encoding:utf-8 -*-
""" 对企业微信发送给企业后台的消息加解密示例代码.
@copyright: Copyright (c) 1998-2014 Tencent Inc.
"""
# ------------------------------------------------------------------------
import base64
import string
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()
sort_str = "".join(sortlist)
sha.update(sort_str.encode('utf-8'))
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception as e:
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
class XMLParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# xml消息模板
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
<TimeStamp>%(timestamp)s</TimeStamp>
<Nonce><![CDATA[%(nonce)s]]></Nonce>
</xml>"""
def extract(self, xmltext):
"""提取出xml数据包中的加密消息
@param xmltext: 待提取的xml字符串
@return: 提取出的加密消息字符串
"""
try:
xml_tree = ET.fromstring(xmltext)
encrypt = xml_tree.find("Encrypt")
touser_name = xml_tree.find("ToUserName")
return ierror.WXBizMsgCrypt_OK, encrypt.text, touser_name.text
except Exception as e:
return ierror.WXBizMsgCrypt_ParseXml_Error, None, None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成xml消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的xml字符串
"""
resp_dict = {
'msg_encrypt': encrypt,
'msg_signaturet': signature,
'timestamp': timestamp,
'nonce': nonce,
}
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_xml
class PKCS7Encoder():
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = chr(amount_to_pad)
return text + pad * amount_to_pad
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, corpid):
"""对明文进行加密
@param text: 需要加密的明文
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = self.get_random_str() + str(struct.pack("I", socket.htonl(len(text))), encoding='utf8')\
+ str(text, encoding='utf8') + corpid
# 使用自定义的填充方式对明文进行补位填充
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:
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
def decrypt(self, text, corpid):
"""对解密后的明文进行补位删除
@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:
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_corpid = content[xml_len + 4:]
except Exception as e:
return ierror.WXBizMsgCrypt_IllegalBuffer, None
if str(from_corpid, encoding="utf8") != corpid and len(ET.fromstring(xml_content).findall("SuiteId")) < 1:
return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None
return 0, xml_content
def get_random_str(self):
""" 随机生成16位字符串
@return: 16位字符串
"""
rule = string.ascii_letters + string.digits
str = random.sample(rule, 16)
return "".join(str)
class WXBizMsgCrypt(object):
# 构造函数
# @param sToken: 企业微信后台开发者设置的Token
# @param sEncodingAESKey: 企业微信后台开发者设置的EncodingAESKey
# @param sCorpId: 企业号的CorpId
def __init__(self, sToken, sEncodingAESKey, sCorpId):
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_sCorpid = sCorpId
# 验证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_sCorpid)
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成功0sEncryptMsg,失败返回对应的错误码None
pc = Prpcrypt(self.key)
ret, encrypt = pc.encrypt(sReplyMsg, self.m_sCorpid)
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, touser_name = 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_sCorpid)
return ret, xml_content

View File

@@ -1,3 +0,0 @@
from . import wechat_enterprise

View File

@@ -1,20 +0,0 @@
#!/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

View File

@@ -1,231 +0,0 @@
import time
import functools
import base64
from json import *
from odoo import http, fields
from odoo.http import request
from . import WXBizMsgCrypt
from werkzeug.exceptions import abort
import xml.etree.cElementTree as Et
import requests as req
import logging
from lxml import etree
_logger = logging.getLogger(__name__)
def wechat_login(func):
"""
用来根据userid取得合作伙伴的id
:return:
"""
@functools.wraps(func)
def wrapper(*args, **kw):
# now_time = time.time()
# request.session['session_time'] = time.time()
# if 'session_time' not in request.session:
# request.session['session_time'] = 1
# if now_time > request.session['session_time'] + 350:
_logger.info(u"没进来之前的kw值为%s" % JSONEncoder().encode(kw))
# if not request.session['login'] or not request.session['login'] or request.session[
# 'login'] == "public":
enterprise, agent_id = request.env['we.config'].sudo().get_odoo_wechat()
if enterprise and agent_id:
if 'code' in kw and 'state' in kw: # 检查是否取得了code
if 'kw' in request.session.keys():
_logger.info('code:%s' % kw['code'])
account = enterprise.oauth.get_user_info(code=kw['code'])
_logger.info('account:%s' % account)
if account: # 是否取得了微信企业号通讯录的账号
user_detail = enterprise.user.get_detail(account['user_ticket'])
_logger.info('user_detail:%s' % user_detail)
request.env['we.employee'].we_privacy_update(user_detail)
user = request.env['res.users'].sudo().search(
[('we_employee_id', '=', account['UserId'])])
_logger.info('user:%s' % user)
if user: # 是否取得了用户
if 'state' in kw:
state = base64.b64encode(kw['state'].encode('utf-8')).decode()
kw['state'] = state
uid = request.session.authenticate(request.session.db, user.login,
account['UserId'])
kw['user_id'] = uid
request.session['session_time'] = time.time()
request.session['login'] = user.login
_logger.info(u"进来之后的kw值为%s" % kw)
return func(*args, **kw)
else:
_logger.warning(u'用户不存在.')
return request.render('sg_wechat_enterprise.wechat_warning',
{'title': u'警告', 'content': u'该员工的未配置登录用户.'})
else:
_logger.warning(u'微信企业号验证失败.')
return request.render('sg_wechat_enterprise.wechat_warning',
{'title': u'警告', 'content': u'微信企业号验证失败.'})
else:
# 返回时候进入的地方
del kw['code']
del kw['state']
request.session['kw'] = base64.b64encode(JSONEncoder().encode(kw).encode('utf-8')).decode()
if len(kw) == 0:
base_url = request.httprequest.base_url
else:
base_url = request.httprequest.base_url + '?'
for item, value in kw.items():
base_url += item + '=' + value + "&"
base_url = base_url[: -1]
url = enterprise.oauth.authorize_url(base_url,
state=base64.b64encode(
JSONEncoder().encode(kw).encode('utf-8')).decode(),
agent_id=agent_id,
scope='snsapi_privateinfo')
_logger.warning(u"这是授权的url:" + url)
value = {"url": url}
return request.render("sg_wechat_enterprise.Transfer", value)
else: # 开始微信企业号登录认证
request.session['kw'] = base64.b64encode(JSONEncoder().encode(kw).encode('utf-8')).decode()
if len(kw) == 0:
base_url = request.httprequest.base_url
else:
base_url = request.httprequest.base_url + '?'
for item, value in kw.items():
base_url += item + '=' + value + "&"
base_url = base_url[: -1]
url = enterprise.oauth.authorize_url(base_url,
state=base64.b64encode(
JSONEncoder().encode(kw).encode('utf-8')).decode(),
agent_id=agent_id,
scope='snsapi_privateinfo'
)
_logger.warning(u"这是授权的url:" + url)
value = {"url": url}
return request.render("sg_wechat_enterprise.Transfer", value)
else:
_logger.warning(u'微信企业号初始化失败.')
return request.render('sg_wechat_enterprise.wechat_warning',
{'title': u'警告', 'content': u'微信企业号初始化失败.'})
# return func(*args, **kw)
return wrapper
class WechatEnterprise(http.Controller):
"""
用于接收微信发过来的任何消息,并转发给相应的业务类进行处理
"""
__check_str = 'NDOEHNDSY#$_@$JFDK:Q{!'
BASE_URL = '/we'
@wechat_login
@http.route(BASE_URL + '/auth', type='http', auth='none')
def auth(self, *args, **kw):
"""
企业微信免登认证
"""
try:
# user_id = (request.session['uid'])
redirect1 = kw['redirect'] if 'redirect' in kw else None
uid = kw['user_id'] if 'redirect' in kw else None
_logger.info('user_id %s', uid)
if uid is not False:
request.params['login_success'] = True
if not redirect1:
redirect1 = '/web'
redirect1 = redirect1.replace('-', '&').replace('?', '#')
logging.info('url:%s' % redirect1)
return request.redirect(redirect1)
except Exception as ex:
_logger.error('无有效的登录凭证.')
_logger.warning('auth exceptions:%s' % ex)
return request.render('sg_wechat_enterprise.wechat_warning',
{'title': u'警告', 'content': u'无有效的登录凭证.'})
@http.route('/WechatEnterprise/<string:code>/api', type='http', auth="public", methods=["GET", "POST"], csrf=False)
def process(self, code, **kwargs):
"""
处理从微信服务器发送过来的请求
:param code: 自定义代码
:param kwargs: 包含 (msg_signature, timestamp, nonce, echostr) 等参数
:return:
"""
_logger.info(u'处理从微信服务器发送过来的请求code: %s, kwargs: %s' % (code, kwargs))
app_id = request.env['we.app'].sudo().search([('code', '=', code)], limit=1)
if not app_id:
_logger.warning(u'Can not find wechat app by code: {code}')
abort(403)
corp_id = app_id.enterprise_id
we_chat_cpt = WXBizMsgCrypt.WXBizMsgCrypt(app_id.Token, app_id.EncodingAESKey, corp_id.corp_id)
signature, timestamp, nonce = kwargs['msg_signature'], kwargs['timestamp'], kwargs['nonce']
if kwargs.get('echostr'):
echo_string = kwargs['echostr']
sort_list = [app_id.Token, timestamp, nonce, echo_string]
if request.env['we.tools'].sudo(). \
check_message_signature(message_list=sort_list, msg_signature=signature):
ret, signature_echo_string = we_chat_cpt.VerifyURL(signature, timestamp, nonce, echo_string)
if ret == 0:
return str(signature_echo_string, encoding="utf8")
body_text = request.httprequest.data
ret, signature_message = we_chat_cpt.DecryptMsg(body_text, signature, timestamp, nonce)
xml_tree = Et.fromstring(signature_message)
if len(xml_tree.findall("SuiteId")) > 0:
return "success"
if len(xml_tree.find("ApprovalInfo")) > 0:
xmlstr = etree.fromstring(signature_message)
# data = xml2json_from_elementtree(xmlstr)
return request.env['we.receive.message'].sudo().sys_approval_change(xml_tree.find("ApprovalInfo"))
data = {
'MsgType': xml_tree.find("MsgType").text,
# 'AgentID': xml_tree.find("AgentID").text,
'ToUserName': xml_tree.find("ToUserName").text,
'FromUserName': xml_tree.find("FromUserName").text,
'CreateTime': xml_tree.find("CreateTime").text
}
if xml_tree.find("AgentID") != None:
data['AgentID'] = xml_tree.find("AgentID").text
if data["MsgType"] == "text":
data["Content"] = xml_tree.find("Content").text
data["MsgId"] = xml_tree.find("MsgId").text
if data["MsgType"] == "image":
data["PicUrl"] = xml_tree.find("PicUrl").text
data["MediaId"] = xml_tree.find("MediaId").text
data["MsgId"] = xml_tree.find("MsgId").text
if data["MsgType"] == "voice":
data["MediaId"] = xml_tree.find("MediaId").text
data["Format"] = xml_tree.find("Format").text
data["MsgId"] = xml_tree.find("MsgId").text
if data["MsgType"] == "video" or data["MsgType"] == "shortvideo":
data["MediaId"] = xml_tree.find("MediaId").text
data["ThumbMediaId"] = xml_tree.find("ThumbMediaId").text
data["MsgId"] = xml_tree.find("MsgId").text
if data["MsgType"] == "location":
data["Location_X"] = xml_tree.find("Location_X").text
data["Location_Y"] = xml_tree.find("Location_Y").text
data["Scale"] = xml_tree.find("Scale").text
data["Label"] = xml_tree.find("Label").text
data["MsgId"] = xml_tree.find("MsgId").text
if data["MsgType"] == "link":
data["Title"] = xml_tree.find("Title").text
data["Description"] = xml_tree.find("Description").text
data["PicUrl"] = xml_tree.find("PicUrl").text
data["MsgId"] = xml_tree.find("MsgId").text
if data["MsgType"] == "event":
if xml_tree.find("Event") == "subscribe" or xml_tree.find("Event") == "unsubscribe":
data["Event"] = xml_tree.find("Event").text
else:
ret, signature_message = we_chat_cpt.EncryptMsg(signature_message, nonce, timestamp)
return signature_message
request.env['we.receive.message'].sudo().process_message(data)
return ''
@http.route('/WechatEnterprise/transfer', type='http', auth="public", methods=["POST", "GET"], csrf=False)
def transfer(self, url):
value = {"url": url}
return request.render('sg_wechat_enterprise.Transfer', value)

View File

@@ -1,16 +0,0 @@
<odoo>
<data>
<record model="ir.cron" id="ir_cron_dingtalk_accesstoken">
<field name="name">SNS Message To 企业微信</field>
<field name="model_id" ref="mail.model_mail_message"/>
<field name="state">code</field>
<field name="code">model.send_we_message()</field>
<field name="interval_number">1</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="user_id" ref="base.user_root"/>
<field name="active" eval="False"/>
</record>
</data>
</odoo>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="main_wechat_enterprise" model="we.config">
<field name="name">SmartGo</field>
<field name="corp_id">wwad4f2c227d490637</field>
<field name="corp_secret">kq_AzJN1FoPdWjyEwAQs_cqzJhALmKhmwYMBQyJzuEs</field>
<field name="company_id">1</field>
</record>
</data>
</openerp>

View File

@@ -1,9 +0,0 @@
from . import we_app
from . import we_conf
from . import we_send_message
from . import we_receive_message
from . import we_receive_message_process
from . import res_users
from . import we_tools
from . import mail
from . import client

View File

@@ -1,11 +0,0 @@
from wechatpy.enterprise import WeChatClient
from . import api
class JkmWeChatClient(WeChatClient):
def __init__(self, corp_id, secret, session=None):
super(JkmWeChatClient, self).__init__(corp_id, secret, session=session)
oauth = api.JkmWechatOauth()
user = api.JkmWechatUser()

View File

@@ -1,2 +0,0 @@
from .jkm_user import JkmWechatUser
from .jkm_oauth import JkmWechatOauth

View File

@@ -1,53 +0,0 @@
from __future__ import absolute_import, unicode_literals
import six
from wechatpy.client.api.base import BaseWeChatAPI
class JkmWechatOauth(BaseWeChatAPI):
OAUTH_BASE_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize'
def authorize_url(self, redirect_uri, state=None, agent_id=None, scope='snsapi_base'):
"""
构造网页授权链接
详情请参考
https://work.weixin.qq.com/api/doc#90000/90135/91022
:param redirect_uri: 授权后重定向的回调链接地址
:param state: 重定向后会带上 state 参数
:param agent_id: 企业应用的id
:param scope: 应用授权作用域
:return: 返回的 JSON 数据包
"""
redirect_uri = six.moves.urllib.parse.quote(redirect_uri, safe=b'')
url_list = [
self.OAUTH_BASE_URL,
'?appid=',
self._client.corp_id,
'&redirect_uri=',
redirect_uri,
'&response_type=code&scope=',
scope,
]
if state:
url_list.extend(['&state=', state])
url_list.append('#wechat_redirect')
if agent_id:
url_list.extend(['&agentid=', str(agent_id)])
return ''.join(url_list)
def get_user_info(self, code):
"""
获取访问用户身份
详情请参考
https://work.weixin.qq.com/api/doc#90000/90135/91023
:param code: 通过成员授权获取到的code
:return: 返回的 JSON 数据包
"""
return self._get(
'user/getuserinfo',
params={
'code': code,
}
)

View File

@@ -1,21 +0,0 @@
from wechatpy.enterprise.client.api import WeChatUser
class JkmWechatUser(WeChatUser):
def get_detail(self, user_ticket):
"""
获取访问用户敏感信息
详情请参考
https://developer.work.weixin.qq.com/document/path/95833
:param user_ticket: 成员票据
:return: 返回的 JSON 数据包
"""
return self._post(
'user/getuserdetail',
data={
'user_ticket': user_ticket
}
)

View File

@@ -1,121 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
import logging
_logger = logging.getLogger(__name__)
# -*- coding: utf-8-*-
import re
##过滤HTML中的标签
#将HTML中标签等信息去掉
#@param htmlstr HTML字符串.
def filter_tags(htmlstr):
#先过滤CDATA
re_cdata=re.compile('//<!\[CDATA\[[^>]*//\]\]>',re.I) #匹配CDATA
re_script=re.compile('<\s*script[^>]*>[^<]*<\s*/\s*script\s*>',re.I)#Script
re_style=re.compile('<\s*style[^>]*>[^<]*<\s*/\s*style\s*>',re.I)#style
re_br=re.compile('<br\s*?/?>')#处理换行
re_h=re.compile('</?\w+[^>]*>')#HTML标签
re_comment=re.compile('<!--[^>]*-->')#HTML注释
s=re_cdata.sub('',htmlstr)#去掉CDATA
s=re_script.sub('',s) #去掉SCRIPT
s=re_style.sub('',s)#去掉style
s=re_br.sub('\n',s)#将br转换为换行
s=re_h.sub('',s) #去掉HTML 标签
s=re_comment.sub('',s)#去掉HTML注释
#去掉多余的空行
blank_line=re.compile('\n+')
s=blank_line.sub('\n',s)
s=replaceCharEntity(s)#替换实体
return s
##替换常用HTML字符实体.
#使用正常的字符替换HTML中特殊的字符实体.
#你可以添加新的实体字符到CHAR_ENTITIES中,处理更多HTML字符实体.
#@param htmlstr HTML字符串.
def replaceCharEntity(htmlstr):
CHAR_ENTITIES={'nbsp':' ','160':' ',
'lt':'<','60':'<',
'gt':'>','62':'>',
'amp':'&','38':'&',
'quot':'"','34':'"',}
re_charEntity=re.compile(r'&#?(?P<name>\w+);')
sz=re_charEntity.search(htmlstr)
while sz:
entity=sz.group()#entity全称如&gt;
key=sz.group('name')#去除&;后entity,如&gt;为gt
try:
htmlstr=re_charEntity.sub(CHAR_ENTITIES[key],htmlstr,1)
sz=re_charEntity.search(htmlstr)
except KeyError:
#以空串代替
htmlstr=re_charEntity.sub('',htmlstr,1)
sz=re_charEntity.search(htmlstr)
return htmlstr
def repalce(s,re_exp,repl_string):
return re_exp.sub(repl_string,s)
class MailMessage(models.Model):
_inherit = 'mail.message'
we_is_send = fields.Selection([('-1', '不需要发送'), ('0', '需要发送'), ('1', '已发送')], string='订订消息发送状态', default='0',
index=True)
we_error_msg = fields.Char('消息不发送原因')
def send_we_message(self):
"""
定时批量发送钉钉消息
:return:
"""
messages = self.search([('we_is_send', '=', '0')])
if messages and len(messages) > 0:
try:
messages.send_message_to_we()
except Exception as ex:
pass
def send_message_to_we(self):
"""
:param agent_id: 必填企业应用的id整型。可在应用的设置页面查看。
:param user_ids: 成员ID列表消息接收者多个接收者用|分隔最多支持1000个
:param title: 标题不超过128个字节超过会自动截断。
:param description: 必填描述不超过512个字节超过会自动截断
:param url: 必填,点击后跳转的链接。
:param btntxt: 按钮文字。 默认为“详情”, 不超过4个文字超过自动截断。
:param party_ids: 部门ID列表。
:param tag_ids: 标签ID列表。
:return:
"""
# 获取配置信息
config = self.env['ir.config_parameter'].sudo()
# 获取wechat
wechat,agent_id = self.env['we.config'].sudo().get_odoo_wechat()
for m in self:
we_user = m.get_we_user()
if we_user and len(we_user)>0:
try:
_logger.info('wechat:%s' % wechat)
model = self.env['ir.model'].search([('model', '=', m.model)], limit=1)
title = "[%s] %s" % (model.name, m.record_name or m.subject or '')
description = filter_tags(m.body) if len(filter_tags(m.body)) > 0 else '有新的状态和数据变更,请注意跟进.'
url = '{base_url}/web#id={id}&model={model}&view_type=form'.format(
base_url=config.get_param('web.base.url'),
id=m.res_id,
model=m.model)
_logger.info('description:%s title:%s' % (description, title))
ret = wechat.message.send_text_card(agent_id, we_user , title, description, url, btntxt='点此跟进..',
party_ids='', tag_ids='')
_logger.info('message_id:%s ret:%s' % (m.message_id, ret))
m.write({'we_is_send':'1','we_error_msg':''})
except Exception as ex:
m.we_error_msg = '向企业微信发送消息失败! 消息编号:%s 原因:%s' % (m.message_id, ex)
_logger.error('向企业微信发送消息失败! 消息编号:%s 原因:%s' % (m.message_id, ex))
else:
m.write({'we_is_send': '-1', 'we_error_msg': '无法发送,没有收件人!'})
_logger.info('message_id:%s 不需要发送,没有收件人!' % m.message_id)
return False

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
import random
from odoo import models, fields, api
from odoo.http import request
from odoo.exceptions import AccessDenied
import logging
_logger = logging.getLogger(__name__)
class ResUsers(models.Model):
_inherit = 'res.users'
we_employee_id = fields.Char(string=u'企业微信账号', default="")
def _check_credentials(self, we_employee_id, env):
"""
用户验证
"""
try:
return super(ResUsers, self)._check_credentials(we_employee_id, env)
except AccessDenied:
user_id = self.env['res.users'].sudo().search([('we_employee_id', '=', we_employee_id)])
if not (user_id and user_id.id == self.env.uid):
raise

View File

@@ -1,213 +0,0 @@
from odoo import api, models, exceptions, fields
import logging
from odoo.http import request
_logger = logging.getLogger(__name__)
class WechatEnterpriseApp(models.Model):
_name = 'we.app'
_description = 'Wechat Enterprise App Manage'
agentid = fields.Integer(string=u'应用ID', required=True)
code = fields.Char(string=u'自定义代码')
name = fields.Char(u'企业号应用名称')
description = fields.Text(u'企业号应用详情')
redirect_domain = fields.Char(u'企业应用可信域名')
isreportuser = fields.Selection([('0', u'不接受'), ('1', u'接收')], u'变更通知', required=True, default='0')
isreportenter = fields.Selection([('0', u'不接受'),
('1', u'接收')], u'进入应用事件上报', required=True, default='0')
enterprise_id = fields.Many2one('we.config', string=u'企业微信')
secret = fields.Char(string=u'Secret', size=100)
home_url = fields.Char(u'主页型应用url')
square_logo_url = fields.Char(u'方形头像url')
round_logo_url = fields.Char(u'圆形头像url')
type = fields.Selection([('1', u'消息型'), ('2', u'主页型')], u'应用类型', required=True, default='1')
allow_userinfos = fields.Char(u'企业应用可见范围(人员)')
allow_partys = fields.Char(u'企业应用可见范围(部门)')
allow_tags = fields.Char(u'企业应用可见范围(标签)')
report_location_flag = fields.Selection([('0', u'不上报'), ('1', u'进入会话上报'), ('2', u'持续上报')], u'位置上报',
required=True, default='1')
logo_mediaid = fields.Char(u'企业应用头像的mediaid')
close = fields.Selection([('0', u''), ('1', u'')], u'是否禁用', required=True, default='0')
app_menu_ids = fields.One2many('we.app.menu', 'agentid', string=u'自定义菜单')
Token = fields.Char(u'Token')
EncodingAESKey = fields.Char(u'EncodingAESKey')
def pull_app_from_we(self, wechat):
"""
从微信获取app列表
:return:
"""
try:
app_lists = wechat.agent.list()
if app_lists:
for app_list in app_lists:
if 'agentid' in app_list:
app_detail = wechat.agent.get(app_list['agentid'])
if app_detail:
data = {
'agentid': str(app_detail['agentid'])
}
my_app = request.env["we.app"].search(
[("agentid", "=", str(app_detail['agentid']))])
if my_app and len(my_app) > 0:
continue
data['name'] = app_detail['name']
data['square_logo_url'] = app_detail['square_logo_url']
data['description'] = app_detail['description']
data['close'] = str(app_detail['close'])
data['redirect_domain'] = app_detail['redirect_domain']
data['report_location_flag'] = str(app_detail['report_location_flag'])
data['isreportenter'] = str(app_detail['isreportenter'])
data['enterprise_id'] = self.id
request.env["we.app"].create(data)
except Exception as e:
raise Warning((u'获取应用列表失败,原因:%s', str(e)))
def push_app_to_we(self):
"""
同步app到微信
:return:
"""
wechat = self.env['we.config'].sudo().get_wechat()
if wechat:
try:
for account in self:
app_json = {
'name': account.name
}
if account.description:
app_json['description'] = account.description
if account.redirect_domain:
app_json['redirect_domain'] = account.redirect_domain
app_json['agentid'] = int(account.agentid)
app_json['report_location_flag'] = int(account.report_location_flag)
if account.type == "1": # 消息型应用
if account.name and account.agentid \
and account.isreportuser and account.isreportenter and account.report_location_flag:
app_json['isreportuser'] = int(account.isreportuser)
app_json['isreportenter'] = int(account.isreportenter)
wechat.agent.set(agent_id=app_json['agentid'], name=app_json['name'],
description=app_json['description'],
redirect_domain=app_json['redirect_domain'],
is_report_user=app_json['isreportuser'],
is_report_enter=app_json['isreportenter'],
report_location_flag=app_json['report_location_flag'])
elif account.type == "2": # 主页型应用
if account.name and account.agentid \
and account.report_location_flag and account.home_url:
app_json['home_url'] = account.home_url
wechat.agent.set(agent_id=app_json['agentid'], name=app_json['name'],
description=app_json['description'],
redirect_domain=app_json['redirect_domain'],
is_report_user=app_json['isreportuser'],
is_report_enter=app_json['isreportenter'],
report_location_flag=app_json['report_location_flag'])
except Exception as e:
_logger.warning(u'更新app失败,原因:%s', str(e))
raise Warning(u'更新app失败,原因:%s', str(e))
else:
raise exceptions.Warning(u"初始化企业号失败")
def update_app_menu(self):
"""
同步菜单至app
:return:
"""
wechat = self.env['we.config'].sudo().get_wechat(agentID=self.agentid)
menus = self.env['we.app.menu'].sudo().search([("agentid", "=", self.name)])
wechat.menu.delete(agent_id=self.agentid)
menu_json = {'button': []}
button = []
if wechat and menus:
for menu in menus:
menu_data = {
'name': menu['name']
}
if not menu['partner_menu_id']:
sub_menus = request.env['we.app.menu'].sudo().search(
[("agentid", "=", self.name), ("partner_menu_id", "=", menu['name'])])
if sub_menus and (len(sub_menus) > 0) and (len(sub_menus) < 6):
sub_menu_list = []
for sub_menu in sub_menus:
sub_menu_data = {
'name': sub_menu['name']
}
if menu['type'] == 'xml' or menu['type'] == 'sub_button':
sub_menu_data['type'] = sub_menu['type']
sub_menu_data['url'] = sub_menu['url']
else:
sub_menu_data['type'] = sub_menu['type']
sub_menu_data['key'] = sub_menu['key']
sub_menu_list.append(sub_menu_data)
menu_data['sub_button'] = sub_menu_list
else:
if menu['type'] == 'xml' or menu['type'] == 'sub_button':
menu_data['type'] = menu['type']
menu_data['url'] = menu['url']
else:
menu_data['type'] = menu['type']
menu_data['key'] = menu['key']
button.append(menu_data)
menu_json['button'] = button
wechat.menu.update(agent_id=self.agentid, menu_data=menu_json)
else:
raise exceptions.Warning(u"初始化企业号失败或该应用无菜单")
class WechatEnterpriseAppMenu(models.Model):
_name = 'we.app.menu'
_description = 'Wechat Enterprise App Menu Manage'
agentid = fields.Many2one('we.app', u'企业应用', required=True)
partner_menu_id = fields.Many2one('we.app.menu', u'上级菜单')
type = fields.Selection(
[('sub_button', u'跳转至子菜单'), ('click', u'点击推事件'), ('xml', u'跳转URL'), ('scancode_push', u'扫码推事件'),
('scancode_waitmsg', u'扫码推事件且弹出“消息接收中”提示框'),
('pic_sysphoto', u'弹出系统拍照发图'), ('pic_photo_or_album', u'弹出拍照或者相册发图'), ('pic_weixin', u'弹出微信相册发图器'),
('location_select', u'弹出地理位置选择器')], u'按钮的类型', required=True, default='xml')
name = fields.Char(u'菜单标题', required=True)
key = fields.Char(u'菜单KEY值')
url = fields.Char(u'网页链接')
@api.constrains('partner_menu_id', 'name')
def _check_menu_name_length(self):
if self.name and self.partner_menu_id and len(self.name) > 7:
raise Warning(u'二级菜单显示名称不能超过14个字符或7个汉字.')
elif self.name and not self.partner_menu_id and len(self.name) > 4:
raise Warning(u'一级菜单显示名称不能超过8个字符或4个汉字.')
else:
return True
@api.constrains('agentid')
def check_menu_number(self):
"""
取得一个app的一级菜单量
:return:
"""
menus_ids = self.sudo().search([('agentid', '=', self.agentid['name']), ('partner_menu_id', '=', False)])
if menus_ids and len(menus_ids) > 3:
raise Warning(u'公众号的一级菜单数据不能超过3个.')
return True
@api.constrains('partner_menu_id')
def check_submenu_number(self):
"""
取得一个一级菜单的子菜单数量
:return:
"""
sub_menus_ids = self.sudo().search(
[('partner_menu_id', '=', self.partner_menu_id['name']), ('partner_menu_id', '!=', False)])
if sub_menus_ids and len(sub_menus_ids) > 5:
raise Warning(u'一级菜单的二级子菜单数据不能超过5个.')
return True

View File

@@ -1,62 +0,0 @@
import logging
import requests
import time
import base64
from odoo.http import request
from odoo import api, models, exceptions, fields, _
from wechatpy.enterprise import WeChatClient
from .client import JkmWeChatClient
from json import *
_logger = logging.getLogger(__name__)
class WechatEnterpriseConfigration(models.Model):
_name = 'we.config'
name = fields.Char(u'名称', required=True, help=u'取个好名字吧!')
corp_id = fields.Char(u'企业ID', required=True, help=u'企业ID必填项')
corp_secret = fields.Char(u'通讯录同步Secret', required=True, help=u'通讯录同步Secret,必填项')
odoo_app_id = fields.Many2one('we.app', u'Odoo应用', help=u'在企业微信工作台配置的与Odoo进行连接的应用')
company_id = fields.Many2one('res.company', string=u'所属公司')
_sql_constraints = [
('code_complete_name_uniq', 'unique (company_id)', '一个所属公司只能定义一个企业微信!')
]
def get_wechat(self, agent_id=None, company_id=1):
"""
取得wechat app的实例
:param agent_id: conf or None (是企业号对象还是应用对象)
:return:
"""
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
if agent_id:
enterprise_app = self.env['we.app'].sudo().search([('agentid', '=', agent_id)])
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise_app.secret)
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise.corp_secret)
def get_odoo_wechat(self, company_id=1):
"""
取得Odoo wechat app的实例
:param agent_id: conf or None (是企业号对象还是应用对象)
:return:
"""
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
if enterprise.odoo_app_id:
return (JkmWeChatClient(corp_id=enterprise.corp_id, secret=enterprise.odoo_app_id.secret),
enterprise.odoo_app_id.agentid)
else:
raise exceptions.Warning(u'Odoo应用未配置. ')
def get_wechat_app(self, app_code=None, company_id=1):
"""
取得wechat app的实例
:param app_code: 应用代码
:return:
"""
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
if app_code:
enterprise_app = self.env['we.app'].sudo().search([('code', '=', app_code)])
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise_app.secret)
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise.corp_secret)

View File

@@ -1,143 +0,0 @@
from odoo import api, models, fields
import logging
from odoo.http import request
import datetime
_logger = logging.getLogger(__name__)
class WechatEnterpriseReceiveMessage(models.Model):
_name = 'we.receive.message'
_description = 'Wechat Enterprise Receive Message Manage'
name = fields.Char(required=True, index=True)
ToUserName = fields.Char(u'企业号CorpID')
FromUserName = fields.Char(u'成员UserID')
CreateTime = fields.Char(u'消息创建时间')
MsgId = fields.Char(u'消息id')
AgentID = fields.Many2one('we.app', u'企业应用')
MsgType = fields.Selection([('text', u'文本'),
('voice', u'语音'),
('image', u'图片'),
('video', u'视频'),
('shortvideo', u'短视频'),
('location', u'位置'),
('link', u'链接'),
('subscribe', u'关注'),
('unsubscribe', u'取消关注'),
('xml', u'自定义菜单链接跳转'),
('click', u'自定义菜单点击'),
('scan', u'扫描二维码'),
('scancode_waitmsg', u'扫描二维码并等待'),
('event', u'取消关注')],
string=u'消息类型', required=True, default='text')
Content = fields.Text(u'文本消息内容')
state = fields.Selection([('1', u'未处理'), ('2', u'已处理'), ('3', u'处理失败')], u'状态', default='1')
PicUrl = fields.Char(u'图片链接')
MediaId = fields.Char(u'图片媒体文件id')
Format = fields.Char(u'语音格式')
ThumbMediaId = fields.Char(u'视频消息缩略图的媒体id')
Location_X = fields.Char(u'地理位置纬度')
Location_Y = fields.Char(u'地理位置经度')
Scale = fields.Char(u'地图缩放大小')
Label = fields.Char(u'地理位置信息')
Title = fields.Char(u'标题')
Description = fields.Char(u'描述')
Cover_PicUrl = fields.Char(u'封面缩略图的url')
@api.depends('MsgType', 'MsgId')
def name_get(self):
result = []
for receive in self:
name = receive.MsgType + '_' + receive.MsgId
result.append((receive.id, name))
return result
def add_message(self, data):
"""
增加一条待处理的上传消息
:param data:
:return:
"""
app = request.env['we.app'].sudo().search([("agentid", "=", data["AgentID"])])
receive_message_data = {
'AgentID': app.id,
'MsgType': data["MsgType"],
'FromUserName': data["FromUserName"],
'ToUserName': data["ToUserName"]
}
current_time = datetime.datetime.now()
real_time = current_time + datetime.timedelta(hours=8)
receive_message_data["CreateTime"] = real_time
receive_message_data["name"] = data["MsgType"] + data["MsgId"]
if data["MsgType"] == "text":
receive_message_data["MsgId"] = data["MsgId"]
receive_message_data["Content"] = data["Content"]
if data["MsgType"] == "image":
receive_message_data["MsgId"] = data["MsgId"]
receive_message_data["PicUrl"] = data["PicUrl"]
receive_message_data["MediaId"] = data["MediaId"]
if data["MsgType"] == "voice":
receive_message_data["MsgId"] = data["MsgId"]
receive_message_data["MediaId"] = data["MediaId"]
receive_message_data["Format"] = data["Format"]
if data["MsgType"] == "video" or data["MsgType"] == "shortvideo":
receive_message_data["MsgId"] = data["MsgId"]
receive_message_data["MediaId"] = data["MediaId"]
receive_message_data["ThumbMediaId"] = data["ThumbMediaId"]
if data["MsgType"] == "location":
receive_message_data["MsgId"] = data["MsgId"]
receive_message_data["Location_X"] = data["Location_X"]
receive_message_data["Location_Y"] = data["Location_Y"]
receive_message_data["Scale"] = data["Scale"]
receive_message_data["Label"] = data["Label"]
if data["MsgType"] == "link":
receive_message_data["MsgId"] = data["MsgId"]
receive_message_data["Title"] = data["Title"]
receive_message_data["Description"] = data["Description"]
receive_message_data["Cover_PicUrl"] = data["PicUrl"]
if data["MsgType"] == "event":
if data["Event"] == "subscribe":
receive_message_data["MsgType"] = "subscribe"
if data["Event"] == "unsubscribe":
receive_message_data["MsgType"] = "unsubscribe"
return super(WechatEnterpriseReceiveMessage, self).create(receive_message_data)
def process_message(self, data):
"""
处理未处理和失败的消息
:param data:
:return:
"""
messages = self.sudo().add_message(data)
for message in messages:
if message:
if message.state == '2':
break
if data["MsgType"] == "text":
process = self.env['we.receive.message.process'].get_message_process(data["MsgType"],
data["Content"],
data["AgentID"])
else:
process = self.env['we.receive.message.process'].get_message_process(data["MsgType"],
" ",
data["AgentID"])
try:
if process:
if data["MsgType"] == "voice" or data["MsgType"] == "image" or data["MsgType"] == "video" or \
data["MsgType"] == "shortvideo":
process.sudo().exec_class_mothed(data["FromUserName"], data["AgentID"], data["MediaId"])
else:
process.sudo().exec_class_mothed(data["FromUserName"], data["AgentID"])
else:
return self.env['we.send.message'].sudo().send_text_message(data["FromUserName"],
data["AgentID"],
content=u'感谢您的关注!')
message.sudo().write({'state': '1'})
except Exception as e:
message.sudo().write({'state': u'处理失败'})
raise Warning(u'处理失败, 原因:%s', str(e))

View File

@@ -1,109 +0,0 @@
from odoo import models, fields
import logging
_logger = logging.getLogger(__name__)
class WechatEnterpriseReceiveMessageProcess(models.Model):
_name = 'we.receive.message.process'
_description = 'Wechat Enterprise Process Receive Process Message'
name = fields.Char(u'名称', help=u'取个好名称方便管理,比如优惠信息索取', required=True)
message_type = fields.Selection([('text', u'文本'),
('voice', u'语音'),
('image', u'图片'),
('video', u'视频'),
('shortvideo', u'短视频'),
('location', u'位置'),
('link', u'链接'),
('subscribe', u'关注'),
('unsubscribe', u'取消关注'),
('xml', u'自定义菜单链接跳转'),
('click', u'自定义菜单点击'),
('scan', u'扫描二维码'),
('scancode_waitmsg', u'扫描二维码并等待'),
('unsubscribe', u'取消关注')],
string=u'消息类型', required=True)
message_key = fields.Char(u'关键字', required=True)
class_name = fields.Char(u'负责处理的类', help='此处填写进行处理的类的名称),例如:topro_service_base.test', required=True, default="类的名称")
method_name = fields.Char(u'负责处理的方法', help='此处填写进入处理的方法名,方法必须包括参数是message和account_id(微信公众号的id),这是一个dict',
required=True, default="方法名")
agentID = fields.Many2one('we.app', u'企业应用', required=True)
note = fields.Text(u'备注')
def get_message_process(self, message_type, key_word=False, agent_id=False):
"""
取得消息处理的设置
:param message_type:
:param key_word:
:param agent_id:
:return:
"""
process = False
if message_type:
process = self.sudo().search(
[('message_type', '=', message_type), ('message_key', '=', key_word), ('agentID', '=', agent_id)],
limit=1)
if not process and message_type and key_word:
process = self.sudo().search([('message_type', '=', message_type), ('message_key', '=', key_word)], limit=1)
if not process and message_type:
process = self.sudo().search([('message_type', '=', message_type)], limit=1)
return process
def exec_by_message_type(self, message_type, message_key, agent_id):
"""
根据消息的类型动态调用类进行执行
:param message_type:
:param message_key:
:param agent_id:
:return:
"""
# 取得对象,
if message_type and message_key:
process = self.get_message_process(message_type, message_key, agent_id)
process.sudo().exec_class_mothed(message_key, agent_id)
def exec_class_mothed(self, from_user_name, agent_id, media_id=None):
"""
执行类的方法
:param from_user_name:
:param agent_id:
:param media_id:
:return:
"""
_logger.debug('exec_class_mothed')
object_function = getattr(self.env[self.class_name], self.method_name)
ret = object_function(from_user_name, agent_id, media_id)
return ret
def hello(self, from_user_name, agent_id):
"""
demo方法模拟动态处理客户的需求
:param from_user_name:
:param agent_id:
:return:
"""
try:
self.env['we.send.message'].sudo(). \
send_news_message(from_user_name, agent_id, u'测试图文信息', u'测试图文信息的描述',
'http://www.baidu.com',
'http://www.kia-hnsyt.com.cn/uploads/allimg/141204/1-1412041624240-L.jpg')
except Exception as e:
_logger.warning(u'发送微信文本失败原因:%s', str(e))
raise Warning(str(e))
def send_img(self, from_user_name, agent_id):
"""
demo方法模拟动态处理客户的需求
:param from_user_name:
:param agent_id:
:return:
"""
try:
self.env['we.send.message'].sudo().send_text_message(from_user_name, agent_id, u'即将为您发送一条消息')
except Exception as e:
_logger.warning(u'发送微信文本失败原因:%s', str(e))
raise Warning(str(e))

View File

@@ -1,108 +0,0 @@
from odoo import api, models, exceptions, fields
import logging
_logger = logging.getLogger(__name__)
class WechatEnterpriseSendMessage(models.Model):
_name = 'we.send.message'
_description = 'Wechat Enterprise Send Message Manage'
touser = fields.Many2many('res.users', 'send_message_to_users_ref',
'wechat_contacts_id', 'touser', u'成员列表')
msgtype = fields.Selection([('text', u'文字消息'), ('image', u'图片消息'), ('voice', u'语音消息'), ('video', u'视频消息'),
('file', u'文件消息'), ('news', u'图文消息'), ('mpnews', u'微信后台图文消息')], u'消息类型',
required=True, default='text')
agentid = fields.Many2one('we.app', u'发送消息的企业应用', required=True)
content = fields.Char(u'消息内容')
media_id = fields.Char(u'媒体文件')
title = fields.Char(u'标题')
description = fields.Text(u'描述')
articles = fields.Char(u'图文消息')
url = fields.Char(u'点击后跳转的链接')
picurl = fields.Char(u'图文消息的图片链接')
thumb_media_id = fields.Char(u'图文消息缩略图')
author = fields.Char(u'图文消息的作者')
content_source_url = fields.Char(u'图文消息点击“阅读原文”之后的页面链接')
news_content = fields.Char(u'图文消息的内容支持html标签')
digest = fields.Char(u'图文消息的描述')
show_cover_pic = fields.Selection([('0', u''), ('1', u'')], u'是否显示封面', default='0')
safe = fields.Selection([('0', u''), ('1', u'')], u'是否是保密消息', required=True, default='0')
def send_message(self):
"""
发送消息给关注企业号的用户
:return:
"""
users = ""
i = 0
if self.touser and len(self.touser) > 0:
for data in self.touser:
i = i + 1
if i == len(self.touser):
if data['we_employee_id']:
users = users + data['we_employee_id']
else:
if data['we_employee_id']:
users = users + data['we_employee_id'] + "|"
if users == "":
users = '@all'
partys = ""
if self.msgtype == "news":
self.send_news_message(users, self.agentid['agentid'], self.title, self.description, self.url, self.picurl)
elif self.msgtype == "text":
self.send_text_message(users, self.agentid['agentid'], self.content, partys)
def send_text_message(self, userid, agentid, content, partyid=None):
"""
发送文本消息给关注企业号的用户
:param userid:
:param agentid:
:param content:
:param partyid:
:return:
"""
try:
wechat = self.env['we.config'].sudo().get_wechat(agent_id=agentid)
if wechat:
data = {
'safe': "0",
'msgtype': 'text',
'agentid': agentid,
'touser': userid,
'toparty': partyid,
'content': content
}
wechat.message.send_text(agent_id=data['agentid'], user_ids=data['touser'], content=data['content'],
party_ids=data['toparty'], safe=data['safe'])
else:
raise exceptions.Warning(u"初始化企业号失败")
except Exception as e:
logging.error('send_text_message:%s' % e)
# 发送图文消息给关注企业号的用户
def send_news_message(self, userid, agentid, title=None, description=None, url=None, picurl=None):
"""
发送图文消息给关注企业号的用户
:param userid:
:param agentid:
:param title:
:param description:
:param url:
:param picurl:
:return:
"""
wechat = self.env['we.config'].sudo().get_wechat(agent_id=agentid)
if wechat:
articles = [
{
'url': url,
'image': picurl,
'description': description,
'title': title
}
]
wechat.message.send_articles(agent_id=agentid, user_ids=userid, articles=articles)
else:
raise exceptions.Warning(u"初始化企业号失败")

View File

@@ -1,89 +0,0 @@
from odoo import api, models, exceptions
from odoo.http import request
import logging
import hashlib
import base64
import time
import requests
_logger = logging.getLogger(__name__)
class WechatEnterpriseTools(models.Model):
"""
微信企业号工具类
"""
_name = 'we.tools'
_description = '微信企业号工具类'
def get_media(self, media_id):
"""
通过media_id 获取媒体文件
:param media_id: media id
:return:
"""
wechat = self.env['we.config'].sudo().get_wechat()
try:
media = wechat.media.download(media_id=media_id)
return {
'errcode': 0,
'errmsg': 'ok',
'media': media.content
}
except Exception as ex:
_logger.info(u'get media fail, message: {str(ex)}.')
return {
'errcode': 30001,
'errmsg': str(ex)
}
def get_jsapi_ticket(self):
"""
获取jsapi_ticket
:return:
"""
if request.session.get('ticket') and request.session.get('ticket_time') \
and int(time.time()) - request.session['ticket_time'] <= 7000:
return {
'errcode': 0,
'errmsg': 'ok',
'ticket': request.session['ticket']
}
wechat = self.env['we.config'].sudo().get_wechat()
get_token = wechat.fetch_access_token()
if get_token['errcode'] == 0 and get_token['errmsg'] == 'ok':
access_token = get_token['access_token']
url = u'https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token={access_token}'
response = requests.get(url).json()
if response['errcode'] == 0 and response['errmsg'] == 'ok':
request.session['ticket'] = response['ticket']
request.session['ticket_time'] = int(time.time())
return response
return {
"errcode": 10002,
"errmsg": "get ticket fail."
}
def check_message_signature(self, message_list, msg_signature):
"""
校验消息的正确性
:param message_list: 消息列表 (list: token, timestamp, nonce, echo_string)
:param msg_signature: 签名
:return: true or false
"""
_logger.info(u'check message signature.')
message_list.sort()
message_str = "".join(message_list)
sha1_message = hashlib.sha1(str(message_str).encode('utf-8')).hexdigest()
if sha1_message == msg_signature:
return True
return False
def decode_echo_str(self, echo_str):
"""
解密echo string 得到明文内容
:param echo_str: 加密字符串
:return: message
"""
_logger.info(u'decode echo string.')
base64_str = base64.b64decode(echo_str)

View File

@@ -1 +0,0 @@
wechatpy==1.8.6

View File

@@ -1,13 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_user_we_app,we_app,model_we_app,,1,1,1,1
access_user_we_config,we_config,model_we_config,,1,1,1,1
access_user_we_receive_message,we_receive_message,model_we_receive_message,,1,1,1,1
access_user_we_receive_message_process,we_receive_message_process,model_we_receive_message_process,,1,1,1,1
access_user_we_send_message,we_send_message,model_we_send_message,,1,1,1,1
access_user_we_app_menu,we_app_menu,model_we_app_menu,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_user_we_app we_app model_we_app 1 1 1 1
3 access_user_we_config we_config model_we_config 1 1 1 1
4 access_user_we_receive_message we_receive_message model_we_receive_message 1 1 1 1
5 access_user_we_receive_message_process we_receive_message_process model_we_receive_message_process 1 1 1 1
6 access_user_we_send_message we_send_message model_we_send_message 1 1 1 1
7 access_user_we_app_menu we_app_menu model_we_app_menu 1 1 1 1

View File

@@ -1,24 +0,0 @@
@-webkit-keyframes loadingCircle {
to {
transform:rotate(1turn)
}
}
@keyframes loadingCircle {
to {
transform:rotate(1turn)
}
}
.load-box {
position:fixed;
top:calc(50% - 11px);
left:calc(50% - 111px);
display:flex;
align-items:center;
}
.load-icon {
animation:loadingCircle 1s linear infinite;
}
.load-txt {
font-size:16px;
margin-left:10px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,26 +0,0 @@
/**
* Created by jiangxiang on 2016/3/14.
*/
//<2F><>ȡ<EFBFBD><C8A1><EFBFBD>Ӵ<EFBFBD><D3B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>openID<49><44><EFBFBD><EFBFBD>
function getUrlParam(url, name) {
var pattern = new RegExp("[?&]" + name + "\=([^&]+)", "g");
var matcher = pattern.exec(url);
var items = null;
if (matcher != null) {
try {
items = decodeURIComponent(decodeURIComponent(matcher[1]));
} catch (e) {
try {
items = decodeURIComponent(matcher[1]);
} catch (e) {
items = matcher[1];
}
}
}
items = items.replace(/^\s*/, "");
return items;
}
var url = document.getElementById("url").innerText;
window.location.href = url;

View File

@@ -1 +0,0 @@
# -*- coding: utf-8 -*-

View File

@@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="view_wechat_enterprise_app_form">
<field name="name">we.app.form</field>
<field name="model">we.app</field>
<field name="arch" type="xml">
<form string="应用配置">
<!--<header>-->
<!--<button string="同步当前应用菜单至微信" name="update_app_menu" type="object" class="oe_highlight"/>-->
<!--</header>-->
<sheet>
<group col="4" string="基础信息">
<field name="name"/>
<field name="enterprise_id" required="1"/>
<field name="agentid"/>
<field name="type"/>
</group>
<group col="4" string="配置信息">
<field name="code"/>
<field name="Token"/>
<field name="EncodingAESKey"/>
<field name="secret" required="1"/>
</group>
<group col="4" string="上报信息设置">
<field name="report_location_flag"/>
<field name="isreportuser" attrs="{'invisible': [('type','in',('2'))]}"/>
<field name="close"/>
<field name="isreportenter" attrs="{'invisible': [('type','in',('2'))]}"/>
</group>
<group col="4" string="更多">
<field name="redirect_domain"/>
<field name="description"/>
<field name="home_url" attrs="{'invisible': [('type','in',('1'))]}"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- tree -->
<record id="view_wechat_enterprise_app_tree" model="ir.ui.view">
<field name="name">we.app.tree</field>
<field name="model">we.app</field>
<field name="arch" type="xml">
<tree string="应用配置">
<field name="agentid"/>
<field name="name"/>
<field name='type'/>
<field name="redirect_domain"/>
<field name="description"/>
</tree>
</field>
</record>
<!-- search -->
<record id="view_wechat_enterprise_app_search" model="ir.ui.view">
<field name="name">we.app.search</field>
<field name="model">we.app</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<search string="应用配置">
<field name="name" filter_domain="[('name','ilike',self)]" string="应用配置"/>
</search>
</field>
</record>
<!-- action -->
<record id="view_wechat_enterprise_app_action" model="ir.actions.act_window">
<field name="name">应用配置</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">we.app</field>
<field name="view_mode">tree,form</field>
<field name="binding_view_types">form</field>
<field name="view_id" ref="view_wechat_enterprise_app_tree"/>
</record>
</data>
</odoo>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- View -->
<record id="view_mail_message_form_dingtalk" model="ir.ui.view">
<field name="name">mail.message.form.dingtalk</field>
<field name="model">mail.message</field>
<field name="inherit_id" ref="mail.view_message_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="企业微信">
<group>
<field name="we_is_send"/>
<field name="we_error_msg"/>
</group>
<footer class="modal-footer">
<button type="object" name="send_message_to_we" string="发送到企业微信"/>
</footer>
</page>
</notebook>
</field>
</record>
</odoo>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem
id="menu_wechat_enterprise"
name="企业微信" parent="base.menu_administration"
sequence="6"/>
<menuitem id="menu_wechat_enterprise_setup" name="设置" sequence="120" parent="menu_wechat_enterprise"/>
<menuitem action="view_wechat_enterprise_config_action" id="menu_wechat_enterprise_config_form" name="企业微信"
parent="menu_wechat_enterprise_setup" sequence="20"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_app_action" id="menu_wechat_enterprise_app_form" name="应用列表"
parent="menu_wechat_enterprise_setup" sequence="20"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_app_menu_action"
id="menu_wechat_enterprise_app_menu_form" name="应用菜单"
parent="menu_wechat_enterprise_setup" sequence="30"/>
<menuitem id="menu_wechat_enterprise_process" name="业务处理" sequence="100" parent="menu_wechat_enterprise"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_receive_message_process_action"
id="menu_wechat_enterprise_receive_message_process_form" name="接收消息处理"
parent="menu_wechat_enterprise_process" sequence="30"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_receive_message_action"
id="menu_wechat_enterprise_receive_message_form" name="接收消息"
parent="menu_wechat_enterprise_process" sequence="30"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_send_message_action"
id="menu_wechat_enterprise_send_message_form" name="发送消息"
parent="menu_wechat_enterprise_process" sequence="30"/>
</odoo>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_users_account_form" model="ir.ui.view">
<field name="name">res.users.account.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<page name="preferences" position="after">
<page name="account" string="企业微信">
<group>
<field name="we_employee_id"/>
</group>
</page>
</page>
</field>
</record>
</data>
</odoo>

View File

@@ -1,149 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="view_wechat_enterprise_app_form">
<field name="name">we.app.form</field>
<field name="model">we.app</field>
<field name="arch" type="xml">
<form string="应用配置">
<!--<header>-->
<!--<button string="同步当前应用菜单至微信" name="update_app_menu" type="object" class="oe_highlight"/>-->
<!--</header>-->
<sheet>
<group col="4" string="基础信息">
<field name="name"/>
<field name="enterprise_id" required="1"/>
<field name="agentid"/>
<field name="type"/>
</group>
<group col="4" string="配置信息">
<field name="code"/>
<field name="Token"/>
<field name="EncodingAESKey"/>
<field name="secret" required="1"/>
</group>
<group col="4" string="上报信息设置">
<field name="report_location_flag"/>
<field name="isreportuser" attrs="{'invisible': [('type','in',('2'))]}"/>
<field name="close"/>
<field name="isreportenter" attrs="{'invisible': [('type','in',('2'))]}"/>
</group>
<group col="4" string="更多">
<field name="redirect_domain"/>
<field name="description"/>
<field name="home_url" attrs="{'invisible': [('type','in',('1'))]}"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- tree -->
<record id="view_wechat_enterprise_app_tree" model="ir.ui.view">
<field name="name">we.app.tree</field>
<field name="model">we.app</field>
<field name="arch" type="xml">
<tree string="应用配置">
<field name="agentid"/>
<field name="name"/>
<field name='type'/>
<field name="redirect_domain"/>
<field name="description"/>
</tree>
</field>
</record>
<!-- search -->
<record id="view_wechat_enterprise_app_search" model="ir.ui.view">
<field name="name">we.app.search</field>
<field name="model">we.app</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<search string="应用配置">
<field name="name" filter_domain="[('name','ilike',self)]" string="应用配置"/>
</search>
</field>
</record>
<!-- action -->
<record id="view_wechat_enterprise_app_action" model="ir.actions.act_window">
<field name="name">应用配置</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">we.app</field>
<field name="view_mode">tree,form</field>
<field name="binding_view_types">form</field>
<field name="view_id" ref="view_wechat_enterprise_app_tree"/>
</record>
<record model="ir.ui.view" id="view_wechat_enterprise_app_menu_form">
<field name="name">we.app.menu.form</field>
<field name="model">we.app.menu</field>
<field name="arch" type="xml">
<form string="应用菜单">
<sheet>
<group>
<group>
<field name="agentid"/>
<field name="type"/>
</group>
<group>
<field name="name"/>
<field name="partner_menu_id"
attrs="{'invisible': [('type','in',('sub_button'))]}"/>
</group>
</group>
<group>
<field name="key" attrs="{'invisible': [('type','in',('view','sub_button'))]}"/>
<field name="url"
attrs="{'invisible': [('type','in',('sub_button','click','scancode_push','scancode_waitmsg','pic_sysphoto','pic_photo_or_album','video'))]}"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- tree -->
<record id="view_wechat_enterprise_app_menu_tree" model="ir.ui.view">
<field name="name">we.app.menu.tree</field>
<field name="model">we.app.menu</field>
<field name="arch" type="xml">
<tree string="应用菜单">
<field name="agentid"/>
<field name="name"/>
<field name="partner_menu_id"/>
<field name="type"/>
<field name="key"/>
<field name="url"/>
</tree>
</field>
</record>
<!-- search -->
<record id="view_wechat_enterprise_app_menu_search" model="ir.ui.view">
<field name="name">we.app.menu.search</field>
<field name="model">we.app.menu</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<search string="应用菜单">
<field name="name" filter_domain="[('name','ilike',self)]" string="菜单名称"/>
</search>
</field>
</record>
<!-- action -->
<record id="view_wechat_enterprise_app_menu_action" model="ir.actions.act_window">
<field name="name">应用菜单</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">we.app.menu</field>
<field name="view_mode">tree,form</field>
<field name="binding_view_types">form</field>
<field name="view_id" ref="view_wechat_enterprise_app_menu_tree"/>
</record>
</data>
</odoo>

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="view_wechat_enterprise_config_form">
<field name="name">we.config.form</field>
<field name="model">we.config</field>
<field name="arch" type="xml">
<form string="企业微信">
<sheet>
<group>
<group>
<field name="name"/>
<field name="company_id" required="1" options="{'no_create_edit': True, 'no_create': True}"/>
<field name="corp_id"/>
<field name="corp_secret"/>
<field name="odoo_app_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- tree -->
<record id="view_wechat_enterprise_config_tree" model="ir.ui.view">
<field name="name">we.config.tree</field>
<field name="model">we.config</field>
<field name="arch" type="xml">
<tree string="企业微信">
<field name="name"/>
<field name='corp_secret'/>
<field name="corp_id"/>
</tree>
</field>
</record>
<!-- action -->
<record id="view_wechat_enterprise_config_action" model="ir.actions.act_window">
<field name="name">企业微信</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">we.config</field>
<field name="view_mode">tree,form</field>
<field name="binding_view_types">form</field>
</record>
</data>
</odoo>

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_wechat_enterprise" name="企业微信" sequence="20" parent="base.menu_administration"
web_icon="sg_wechat_enterprise,static/description/qyh.png">
<menuitem id="menu_wechat_enterprise_setup" name="设置" sequence="120">
<menuitem action="view_wechat_enterprise_config_action" id="menu_wechat_enterprise_config_form" name="企业微信"
parent="menu_wechat_enterprise_setup" sequence="20"/>
<menuitem action="view_wechat_enterprise_app_action" id="menu_wechat_enterprise_app_form" name="应用列表"
sequence="20"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_app_action" id="menu_wechat_enterprise_app_form" name="应用列表"
parent="menu_wechat_enterprise_setup" sequence="20"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_app_menu_action"
id="menu_wechat_enterprise_app_menu_form" name="应用菜单"
parent="menu_wechat_enterprise_setup" sequence="30"/>
</menuitem>
<menuitem id="menu_wechat_enterprise_contact" name="通讯录" sequence="110"/>
<menuitem id="menu_wechat_enterprise_process" name="业务处理" sequence="100">
<!-- menu -->
<menuitem action="view_wechat_enterprise_receive_message_process_action"
id="menu_wechat_enterprise_receive_message_process_form" name="接收消息处理"
parent="menu_wechat_enterprise_process" sequence="30"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_receive_message_action"
id="menu_wechat_enterprise_receive_message_form" name="接收消息"
parent="menu_wechat_enterprise_process" sequence="30"/>
<!-- menu -->
<menuitem action="view_wechat_enterprise_send_message_action"
id="menu_wechat_enterprise_send_message_form" name="发送消息"
parent="menu_wechat_enterprise_process" sequence="30"/>
</menuitem>
</menuitem>
</odoo>

View File

@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="view_wechat_enterprise_receive_message_process_form">
<field name="name">we.receive.message.process.form</field>
<field name="model">we.receive.message.process</field>
<field name="arch" type="xml">
<form string="接收消息处理">
<sheet>
<group>
<group>
<field name="name"/>
<field name="message_key"/>
<field name="class_name"/>
</group>
<group>
<field name="message_type"/>
<field name="agentID"/>
<field name="method_name"/>
</group>
</group>
<group>
<field name="note"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- tree -->
<record id="view_wechat_enterprise_receive_message_process_tree" model="ir.ui.view">
<field name="name">we.receive.message.process.tree</field>
<field name="model">we.receive.message.process</field>
<field name="arch" type="xml">
<tree string="接收消息处理">
<field name="name"/>
<field name="message_key"/>
<field name="class_name"/>
<field name="method_name"/>
<field name="message_type"/>
<field name="agentID"/>
</tree>
</field>
</record>
<!-- search -->
<record id="view_wechat_enterprise_receive_message_process_search" model="ir.ui.view">
<field name="name">we.receive.message.process.search</field>
<field name="model">we.receive.message.process</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<search string="接收消息处理">
<field name="message_key" filter_domain="[('message_key','ilike',self)]" string="关键字"/>
</search>
</field>
</record>
<!-- action -->
<record id="view_wechat_enterprise_receive_message_process_action" model="ir.actions.act_window">
<field name="name">接收消息处理</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">we.receive.message.process</field>
<field name="view_mode">tree,form</field>
<field name="binding_view_types">form</field>
<field name="view_id" ref="view_wechat_enterprise_receive_message_process_tree"/>
</record>
</data>
</odoo>

View File

@@ -1,98 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="view_wechat_enterprise_receive_message_form">
<field name="name">we.receive.message.form</field>
<field name="model">we.receive.message</field>
<field name="arch" type="xml">
<form string="接收消息">
<sheet>
<group string="消息基本信息">
<group>
<field name="ToUserName"/>
<field name="AgentID"/>
<field name="MsgId"/>
</group>
<group>
<field name="FromUserName"/>
<field name="state"/>
<field name="CreateTime"/>
</group>
</group>
<group>
<field name="MsgType"/>
</group>
<group string="消息内容">
<field name="MediaId" attrs="{'invisible': [('MsgType','in',('text','location','link'))]}"/>
<field name="Format" attrs="{'invisible': [('MsgType','in',('text','image','video','shortvideo','location','link'))]}"/>
<field name="ThumbMediaId" attrs="{'invisible': [('MsgType','in',('text','image','voice','location','link'))]}"/>
<field name="PicUrl" attrs="{'invisible': [('MsgType','in',('text','voice','video','shortvideo','location','link'))]}"/>
<field name="Content" attrs="{'invisible': [('MsgType','in',('image','voice','video','location','link','shortvideo'))]}"/>
</group>
<group>
<group>
<field name="Location_X" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','link'))]}"/>
<field name="Scale" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','link'))]}"/>
</group>
<group>
<field name="Location_Y" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','link'))]}"/>
<field name="Label" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','link'))]}"/>
</group>
</group>
<group>
<group>
<field name="Title" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','location'))]}"/>
<field name="Description" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','location'))]}"/>
</group>
<group>
<field name="Cover_PicUrl" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','location'))]}"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- tree -->
<record id="view_wechat_enterprise_receive_message_tree" model="ir.ui.view">
<field name="name">we.receive.message.tree</field>
<field name="model">we.receive.message</field>
<field name="arch" type="xml">
<tree string="接收消息">
<field name="ToUserName"/>
<field name="CreateTime"/>
<field name="FromUserName"/>
<field name="MsgId"/>
<field name="AgentID"/>
<field name="MsgType"/>
<field name="Content"/>
<field name="state"/>
</tree>
</field>
</record>
<!-- search -->
<record id="view_wechat_enterprise_receive_message_search" model="ir.ui.view">
<field name="name">we.receive.message.search</field>
<field name="model">we.receive.message</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<search string="接收消息">
<field name="Content" filter_domain="[('Content','ilike',self)]" string="消息内容"/>
</search>
</field>
</record>
<!-- action -->
<record id="view_wechat_enterprise_receive_message_action" model="ir.actions.act_window">
<field name="name">接收消息</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">we.receive.message</field>
<field name="view_mode">tree,form</field>
<field name="binding_view_types">form</field>
<field name="view_id" ref="view_wechat_enterprise_receive_message_tree"/>
</record>
</data>
</odoo>

View File

@@ -1,105 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="view_wechat_enterprise_send_message_form">
<field name="name">we.send.message.form</field>
<field name="model">we.send.message</field>
<field name="arch" type="xml">
<form string="发送消息">
<header>
<button string="发送本条消息" name="send_message" type="object" class="oe_highlight"/>
</header>
<sheet>
<group string="消息内容">
<group>
<field name="msgtype"/>
</group>
<group>
<field name="agentid"/>
</group>
</group>
<group>
<group>
<field name="content"
attrs="{'invisible': [('msgtype','in',('image','voice','video','file','news','mpnews'))]}"/>
</group>
<group>
<field name="media_id"
attrs="{'invisible': [('msgtype','in',('text','news','mpnews'))]}"/>
</group>
</group>
<group>
<field name="title"
attrs="{'invisible': [('msgtype','in',('image','voice','text','file'))]}"/>
<field name="description"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','mpnews'))]}"/>
</group>
<group>
<field name="url"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','mpnews','video'))]}"/>
<field name="picurl"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','mpnews','video'))]}"/>
</group>
<group>
<field name="thumb_media_id"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
<field name="author"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
<field name="content_source_url"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
<field name="news_content"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
<field name="digest"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
<field name="show_cover_pic"
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
</group>
<group string="消息发送的范围(若不填,则默认发送全体员工)">
<field name="touser"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- tree -->
<record id="view_wechat_enterprise_send_message_tree" model="ir.ui.view">
<field name="name">we.send.message.tree</field>
<field name="model">we.send.message</field>
<field name="arch" type="xml">
<tree string="发送消息">
<field name="touser"/>
<field name="msgtype"/>
<field name="agentid"/>
<field name="content"/>
</tree>
</field>
</record>
<!-- search -->
<record id="view_wechat_enterprise_send_message_search" model="ir.ui.view">
<field name="name">we.send.message.search</field>
<field name="model">we.send.message</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<search string="发送消息">
<field name="content" filter_domain="[('content','ilike',self)]" string="文字消息内容"/>
<field name="title" filter_domain="[('title','ilike',self)]" string="图文消息标题"/>
<field name="description" filter_domain="[('description','ilike',self)]" string="图文消息描述"/>
</search>
</field>
</record>
<!-- action -->
<record id="view_wechat_enterprise_send_message_action" model="ir.actions.act_window">
<field name="name">发送消息</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">we.send.message</field>
<field name="view_mode">tree,form</field>
<field name="binding_view_types">form</field>
<field name="view_id" ref="view_wechat_enterprise_send_message_tree"/>
</record>
</data>
</odoo>

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<template id="sg_wechat_enterprise.layout" name="Wechat_Enterprise_Layout">
&lt;!DOCTYPE html&gt;
<html style="height:100%">
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<t t-raw="head or ''"/>
</head>
<body>
<t t-raw="0"/>
</body>
</html>
</template>
<template id="sg_wechat_enterprise.Transfer" name="Wechat_Enterprise_Transfer">
<t t-call="sg_wechat_enterprise.layout">
<t t-set="head">
</t>
<p id="url" style="display:none">
<t t-raw="url" />
</p>
<div class="load-box">
<svg class="load-icon" viewBox="0 0 1024 1024" data-icon="loading" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false"><path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path></svg>
<span class="load-txt">页面正在加载中,请耐心等候。</span>
</div>
<link rel="stylesheet" href="/sg_wechat_enterprise/static/css/loading.css"/>
<script src="/sg_wechat_enterprise/static/js/url_transfers.js"></script>
</t>
</template>
<template id="sg_wechat_enterprise.wechat_warning" name="Wechat_Enterprise_arning">
<t t-call="sg_wechat_enterprise.layout">
<t t-set="head">
</t>
<div class="mr_center">
<p><h3><t t-esc="title"/></h3></p>
<p><t t-esc="content" /></p>
</div>
</t>
</template>
</data>
</odoo>

View File

@@ -1,33 +0,0 @@
from __future__ import absolute_import, unicode_literals
import logging
try:
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
except ImportError:
from pkg_resources import declare_namespace
declare_namespace(__name__)
from wechatpy.parser import parse_message # NOQA
from wechatpy.replies import create_reply # NOQA
from wechatpy.client import WeChatClient # NOQA
from wechatpy.exceptions import WeChatException # NOQA
from wechatpy.exceptions import WeChatClientException # NOQA
from wechatpy.oauth import WeChatOAuth # NOQA
from wechatpy.exceptions import WeChatOAuthException # NOQA
from wechatpy.pay import WeChatPay # NOQA
from wechatpy.exceptions import WeChatPayException # NOQA
from wechatpy.component import WeChatComponent # NOQA
__version__ = '1.3.1'
__author__ = 'messense'
# Set default logging handler to avoid "No handler found" warnings.
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
logging.getLogger(__name__).addHandler(NullHandler())

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
"""
wechatpy._compat
~~~~~~~~~~~~~~~~~
This module makes it easy for wechatpy to run on both Python 2 and 3.
:copyright: (c) 2014 by messense.
:license: MIT, see LICENSE for more details.
"""
from __future__ import absolute_import, unicode_literals
import sys
import six
import warnings
warnings.warn("Module `wechatpy._compat` is deprecated, will be removed in 2.0"
"use `wechatpy.utils` instead",
DeprecationWarning, stacklevel=2)
from wechatpy.utils import get_querystring
from wechatpy.utils import json

View File

@@ -1,140 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
try:
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
except ImportError:
from pkg_resources import declare_namespace
declare_namespace(__name__)
import time
from wechatpy.client.base import BaseWeChatClient
from wechatpy.client import api
class WeChatClient(BaseWeChatClient):
"""
微信 API 操作类
通过这个类可以操作微信 API发送主动消息、群发消息和创建自定义菜单等。
"""
API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin/'
menu = api.WeChatMenu()
user = api.WeChatUser()
group = api.WeChatGroup()
media = api.WeChatMedia()
card = api.WeChatCard()
qrcode = api.WeChatQRCode()
message = api.WeChatMessage()
misc = api.WeChatMisc()
merchant = api.WeChatMerchant()
customservice = api.WeChatCustomService()
datacube = api.WeChatDataCube()
jsapi = api.WeChatJSAPI()
material = api.WeChatMaterial()
semantic = api.WeChatSemantic()
shakearound = api.WeChatShakeAround()
device = api.WeChatDevice()
template = api.WeChatTemplate()
poi = api.WeChatPoi()
wifi = api.WeChatWiFi()
scan = api.WeChatScan()
def __init__(self, appid, secret, access_token=None,
session=None, timeout=None, auto_retry=True):
super(WeChatClient, self).__init__(
appid, access_token, session, timeout, auto_retry
)
self.appid = appid
self.secret = secret
def fetch_access_token(self):
"""
获取 access token
详情请参考 http://mp.weixin.qq.com/wiki/index.php?title=通用接口文档
:return: 返回的 JSON 数据包
"""
return self._fetch_access_token(
url='https://api.weixin.qq.com/cgi-bin/token',
params={
'grant_type': 'client_credential',
'appid': self.appid,
'secret': self.secret
}
)
class WeChatComponentClient(WeChatClient):
"""
开放平台代公众号调用客户端
"""
def __init__(self, appid, component, access_token=None,
refresh_token=None, session=None, timeout=None):
# 未用到secret所以这里没有
super(WeChatComponentClient, self).__init__(
appid, '', access_token, session, timeout
)
self.appid = appid
self.component = component
# 如果公众号是刚授权外部还没有缓存access_token和refresh_token
# 可以传入这两个值session 会缓存起来。
# 如果外部已经缓存,这里只需要传入 appidcomponent和session即可
if access_token:
self.session.set(self.access_token_key, access_token, 7200)
if refresh_token:
self.session.set(self.refresh_token_key, refresh_token, 7200)
@property
def access_token_key(self):
return '{0}_access_token'.format(self.appid)
@property
def refresh_token_key(self):
return '{0}_refresh_token'.format(self.appid)
@property
def access_token(self):
access_token = self.session.get(self.access_token_key)
if not access_token:
self.fetch_access_token()
access_token = self.session.get(self.access_token_key)
return access_token
@property
def refresh_token(self):
return self.session.get(self.refresh_token_key)
def fetch_access_token(self):
"""
获取 access token
详情请参考 https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list\
&t=resource/res_list&verify=1&id=open1419318587&token=&lang=zh_CN
这是内部刷新机制。请不要完全依赖!
因为有可能在缓存期间没有对此公众号的操作造成refresh_token失效。
:return: 返回的 JSON 数据包
"""
expires_in = 7200
result = self.component.refresh_authorizer_token(
self.appid, self.refresh_token)
if 'expires_in' in result:
expires_in = result['expires_in']
self.session.set(
self.access_token_key,
result['authorizer_access_token'],
expires_in
)
self.session.set(
self.refresh_token_key,
result['authorizer_refresh_token'],
expires_in
)
self.expires_at = int(time.time()) + expires_in
return result

View File

@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.client.api.menu import WeChatMenu # NOQA
from wechatpy.client.api.user import WeChatUser # NOQA
from wechatpy.client.api.card import WeChatCard # NOQA
from wechatpy.client.api.group import WeChatGroup # NOQA
from wechatpy.client.api.media import WeChatMedia # NOQA
from wechatpy.client.api.message import WeChatMessage # NOQA
from wechatpy.client.api.qrcode import WeChatQRCode # NOQA
from wechatpy.client.api.misc import WeChatMisc # NOQA
from wechatpy.client.api.merchant import WeChatMerchant # NOQA
from wechatpy.client.api.customservice import WeChatCustomService # NOQA
from wechatpy.client.api.datacube import WeChatDataCube # NOQA
from wechatpy.client.api.jsapi import WeChatJSAPI # NOQA
from wechatpy.client.api.material import WeChatMaterial # NOQA
from wechatpy.client.api.semantic import WeChatSemantic # NOQA
from wechatpy.client.api.shakearound import WeChatShakeAround # NOQA
from wechatpy.client.api.device import WeChatDevice # NOQA
from wechatpy.client.api.template import WeChatTemplate # NOQA
from wechatpy.client.api.poi import WeChatPoi # NOQA
from wechatpy.client.api.wifi import WeChatWiFi # NOQA
from wechatpy.client.api.scan import WeChatScan # NOQA

View File

@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
class BaseWeChatAPI(object):
API_BASE_URL = ''
""" WeChat API base class """
def __init__(self, client=None):
self._client = client
def _get(self, url, **kwargs):
if getattr(self, 'API_BASE_URL', None):
kwargs['api_base_url'] = self.API_BASE_URL
return self._client.get(url, **kwargs)
def _post(self, url, **kwargs):
if getattr(self, 'API_BASE_URL', None):
kwargs['api_base_url'] = self.API_BASE_URL
return self._client.post(url, **kwargs)
@property
def access_token(self):
return self._client.access_token
@property
def session(self):
return self._client.session

View File

@@ -1,431 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatCard(BaseWeChatAPI):
API_BASE_URL = 'https://api.weixin.qq.com/'
def create(self, card_data):
"""
创建卡券
:param card_data: 卡券信息
:return: 创建的卡券 ID
"""
result = self._post(
'card/create',
data=card_data,
result_processor=lambda x: x['card_id']
)
return result
def batch_add_locations(self, location_data):
"""
批量导入门店信息
:param location_data: 门店信息
:return: 门店 ID 列表,插入失败的门店元素值为 -1
"""
result = self._post(
'card/location/batchadd',
data=location_data,
result_processor=lambda x: x['location_id_list']
)
return result
def batch_get_locations(self, offset=0, count=0):
"""
批量获取门店信息
"""
return self._post(
'card/location/batchget',
data={
'offset': offset,
'count': count
}
)
def get_colors(self):
"""
获得卡券的最新颜色列表,用于创建卡券
:return: 颜色列表
"""
result = self._get(
'card/getcolors',
result_processor=lambda x: x['colors']
)
return result
def create_qrcode(self, qrcode_data):
"""
创建卡券二维码
:param qrcode_data: 二维码信息
:return: 二维码 ticket可使用 :func:show_qrcode 换取二维码文件
"""
result = self._post(
'card/qrcode/create',
data=qrcode_data,
result_processor=lambda x: x['ticket']
)
return result
def create_landingpage(self, buffer_data):
"""
创建货架
"""
result = self._post(
'card/landingpage/create',
data=buffer_data
)
return result
def get_html(self, card_id):
"""
图文消息群发卡券
"""
result = self._post(
'card/mpnews/gethtml',
data={
'card_id': card_id
},
result_processor=lambda x: x['content']
)
return result
def consume_code(self, code, card_id=None):
"""
消耗 code
"""
card_data = {
'code': code
}
if card_id:
card_data['card_id'] = card_id
return self._post(
'card/code/consume',
data=card_data
)
def decrypt_code(self, encrypt_code):
"""
解码加密的 code
"""
result = self._post(
'card/code/decrypt',
data={
'encrypt_code': encrypt_code
},
result_processor=lambda x: x['code']
)
return result
def delete(self, card_id):
"""
删除卡券
"""
return self._post(
'card/delete',
data={
'card_id': card_id
}
)
def get_code(self, code, card_id=None, check_consume=True):
"""
查询 code 信息
"""
card_data = {
'code': code
}
if card_id:
card_data['card_id'] = card_id
if not check_consume:
card_data['check_consume'] = check_consume
return self._post(
'card/code/get',
data=card_data
)
def get_card_list(self, openid, card_id=None):
"""
用于获取用户卡包里的属于该appid下的卡券。
"""
card_data = {
'openid': openid
}
if card_id:
card_data['card_id'] = card_id
return self._post(
'card/user/getcardlist',
data=card_data
)
def batch_get(self, offset=0, count=50, status_list=None):
"""
批量查询卡券信息
"""
card_data = {
'offset': offset,
'count': count
}
if status_list:
card_data['status_list'] = status_list
return self._post(
'card/batchget',
data=card_data
)
def get(self, card_id):
"""
查询卡券详情
"""
result = self._post(
'card/get',
data={
'card_id': card_id
},
result_processor=lambda x: x['card']
)
return result
def update_code(self, card_id, old_code, new_code):
"""
更新卡券 code
"""
return self._post(
'card/code/update',
data={
'card_id': card_id,
'code': old_code,
'new_code': new_code
}
)
def invalid_code(self, code, card_id=None):
"""
设置卡券失效
"""
card_data = {
'code': code
}
if card_id:
card_data['card_id'] = card_id
return self._post(
'card/code/unavailable',
data=card_data
)
def update(self, card_data):
"""
更新卡券信息
"""
return self._post(
'card/update',
data=card_data
)
def set_paycell(self, card_id, is_open):
"""
更新卡券信息
"""
return self._post(
'card/paycell/set',
data={
'card_id': card_id,
'is_open': is_open
}
)
def set_test_whitelist(self, openids=None, usernames=None):
"""
设置卡券测试用户白名单
"""
openids = openids or []
usernames = usernames or []
return self._post(
'card/testwhitelist/set',
data={
'openid': openids,
'username': usernames
}
)
def activate_membercard(self, membership_number, code, init_bonus=0,
init_balance=0, card_id=None):
"""
激活/绑定会员卡
"""
card_data = {
'membership_number': membership_number,
'code': code,
'init_bonus': init_bonus,
'init_balance': init_balance
}
if card_id:
card_data['card_id'] = card_id
return self._post(
'card/membercard/activate',
data=card_data
)
def update_membercard(self, code, add_bonus=0, record_bonus='',
add_balance=0, record_balance='', card_id=None):
"""
会员卡交易更新信息
"""
card_data = {
'code': code,
'add_bonus': add_bonus,
'add_balance': add_balance,
'record_bonus': record_bonus,
'record_balance': record_balance
}
if card_id:
card_data['card_id'] = card_id
return self._post(
'card/membercard/updateuser',
data=card_data
)
def update_movie_ticket(self, code, ticket_class, show_time, duration,
screening_room, seat_number, card_id=None):
"""
更新电影票
"""
ticket = {
'code': code,
'ticket_class': ticket_class,
'show_time': show_time,
'duration': duration,
'screening_room': screening_room,
'seat_number': seat_number
}
if card_id:
ticket['card_id'] = card_id
return self._post(
'card/movieticket/updateuser',
data=ticket
)
def checkin_boardingpass(self, code, passenger_name, seat_class,
etkt_bnr, seat='', gate='', boarding_time=None,
is_cancel=False, qrcode_data=None, card_id=None):
"""
飞机票接口
"""
data = {
'code': code,
'passenger_name': passenger_name,
'class': seat_class,
'etkt_bnr': etkt_bnr,
'seat': seat,
'gate': gate,
'is_cancel': is_cancel
}
if boarding_time:
data['boarding_time'] = boarding_time
if qrcode_data:
data['qrcode_data'] = qrcode_data
if card_id:
data['card_id'] = card_id
return self._post(
'card/boardingpass/checkin',
data=data
)
def update_luckymoney_balance(self, code, balance, card_id=None):
"""
更新红包余额
"""
card_data = {
'code': code,
'balance': balance
}
if card_id:
card_data['card_id'] = card_id
return self._post(
'card/luckymoney/updateuserbalance',
data=card_data
)
def get_redirect_url(self, url, encrypt_code, card_id):
"""
获取卡券跳转外链
"""
from wechatpy.utils import WeChatSigner
code = self.decrypt_code(encrypt_code)
signer = WeChatSigner()
signer.add_data(self.secret)
signer.add_data(code)
signer.add_data(card_id)
signature = signer.signature
r = '{url}?encrypt_code={code}&card_id={card_id}&signature={signature}'
return r.format(
url=url,
code=encrypt_code,
card_id=card_id,
signature=signature
)
def deposit_code(self, card_id, codes):
"""
导入code
"""
card_data = {
'card_id': card_id,
'code': codes
}
return self._post(
'card/code/deposit',
data=card_data
)
def get_deposit_count(self, card_id):
"""
查询导入code数目
"""
card_data = {
'card_id': card_id,
}
return self._post(
'card/code/getdepositcount',
data=card_data
)
def check_code(self, card_id, codes):
"""
核查code
"""
card_data = {
'card_id': card_id,
'code': codes
}
return self._post(
'card/code/checkcode',
data=card_data
)
def modify_stock(self, card_id, n):
"""
修改库存
"""
if n == 0:
return
card_data = {
'card_id': card_id,
}
if n > 0:
card_data['increase_stock_value'] = n
elif n < 0:
card_data['reduce_stock_value'] = -n
return self._post(
'card/modifystock',
data=card_data
)

View File

@@ -1,242 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import hashlib
import time
import datetime
from six.moves.urllib.parse import quote
from optionaldict import optionaldict
from wechatpy.utils import to_binary
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatCustomService(BaseWeChatAPI):
def add_account(self, account, nickname, password):
"""
添加客服账号
详情请参考
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
:param account: 完整客服账号,格式为:账号前缀@公众号微信号
:param nickname: 客服昵称最长6个汉字或12个英文字符
:param password: 客服账号登录密码
:return: 返回的 JSON 数据包
"""
password = to_binary(password)
password = hashlib.md5(password).hexdigest()
return self._post(
'https://api.weixin.qq.com/customservice/kfaccount/add',
data={
'kf_account': account,
'nickname': nickname,
'password': password
}
)
def update_account(self, account, nickname, password):
"""
更新客服账号
详情请参考
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
:param account: 完整客服账号,格式为:账号前缀@公众号微信号
:param nickname: 客服昵称最长6个汉字或12个英文字符
:param password: 客服账号登录密码
:return: 返回的 JSON 数据包
"""
password = to_binary(password)
password = hashlib.md5(password).hexdigest()
return self._post(
'https://api.weixin.qq.com/customservice/kfaccount/update',
data={
'kf_account': account,
'nickname': nickname,
'password': password
}
)
def delete_account(self, account):
"""
删除客服账号
详情请参考
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
:param account: 完整客服账号,格式为:账号前缀@公众号微信号
:return: 返回的 JSON 数据包
"""
params_data = [
'access_token={0}'.format(quote(self.access_token)),
'kf_account={0}'.format(quote(to_binary(account), safe=b'/@')),
]
params = '&'.join(params_data)
return self._get(
'https://api.weixin.qq.com/customservice/kfaccount/del',
params=params
)
def get_accounts(self):
"""
获取客服账号列表
详情请参考
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
:return: 客服账号列表
"""
res = self._get(
'customservice/getkflist',
result_processor=lambda x: x['kf_list']
)
return res
def upload_headimg(self, account, media_file):
"""
上传客服账号头像
详情请参考
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
:param account: 完整客服账号
:param media_file: 要上传的头像文件,一个 File-Object
:return: 返回的 JSON 数据包
"""
return self._post(
'https://api.weixin.qq.com/customservice/kfaccount/uploadheadimg',
params={
'kf_account': account
},
files={
'media': media_file
}
)
def get_online_accounts(self):
"""
获取在线客服接待信息
详情请参考
http://mp.weixin.qq.com/wiki/9/6fff6f191ef92c126b043ada035cc935.html
:return: 客服接待信息列表
"""
res = self._get(
'customservice/getonlinekflist',
result_processor=lambda x: x['kf_online_list']
)
return res
def create_session(self, openid, account, text=None):
"""
多客服创建会话
详情请参考
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
:param openid: 客户 openid
:param account: 完整客服账号
:param text: 附加信息,可选
:return: 返回的 JSON 数据包
"""
data = optionaldict(
openid=openid,
kf_account=account,
text=text
)
return self._post(
'https://api.weixin.qq.com/customservice/kfsession/create',
data=data
)
def close_session(self, openid, account, text=None):
"""
多客服关闭会话
详情请参考
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
:param openid: 客户 openid
:param account: 完整客服账号
:param text: 附加信息,可选
:return: 返回的 JSON 数据包
"""
data = optionaldict(
openid=openid,
kf_account=account,
text=text
)
return self._post(
'https://api.weixin.qq.com/customservice/kfsession/close',
data=data
)
def get_session(self, openid):
"""
获取客户的会话状态
详情请参考
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
:param openid: 客户 openid
:return: 返回的 JSON 数据包
"""
return self._get(
'https://api.weixin.qq.com/customservice/kfsession/getsession',
params={'openid': openid}
)
def get_session_list(self, account):
"""
获取客服的会话列表
详情请参考
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
:param account: 完整客服账号
:return: 客服的会话列表
"""
res = self._get(
'https://api.weixin.qq.com/customservice/kfsession/getsessionlist',
params={'kf_account': account},
result_processor=lambda x: x['sessionlist']
)
return res
def get_wait_case(self):
"""
获取未接入会话列表
详情请参考
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
:return: 返回的 JSON 数据包
"""
return self._get(
'https://api.weixin.qq.com/customservice/kfsession/getwaitcase'
)
def get_records(self, start_time, end_time, page_index,
page_size=10, user_id=None):
"""
获取客服聊天记录
详情请参考
http://mp.weixin.qq.com/wiki/19/7c129ec71ddfa60923ea9334557e8b23.html
:param start_time: 查询开始时间UNIX 时间戳
:param end_time: 查询结束时间UNIX 时间戳,每次查询不能跨日查询
:param page_index: 查询第几页,从 1 开始
:param page_size: 每页大小,每页最多拉取 1000 条
:param user_id: 普通用户的标识,对当前公众号唯一
:return: 返回的 JSON 数据包
"""
if isinstance(start_time, datetime.datetime):
start_time = time.mktime(start_time.timetuple())
if isinstance(end_time, datetime.datetime):
end_time = time.mktime(end_time.timetuple())
record_data = {
'starttime': int(start_time),
'endtime': int(end_time),
'pageindex': page_index,
'pagesize': page_size
}
if user_id:
record_data['openid'] = user_id
res = self._post(
'https://api.weixin.qq.com/customservice/msgrecord/getrecord',
data=record_data,
result_processor=lambda x: x['recordlist']
)
return res

View File

@@ -1,360 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import datetime
import six
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatDataCube(BaseWeChatAPI):
API_BASE_URL = 'https://api.weixin.qq.com/datacube/'
@classmethod
def _to_date_str(cls, date):
if isinstance(date, (datetime.datetime, datetime.date)):
return date.strftime('%Y-%m-%d')
elif isinstance(date, six.string_types):
return date
else:
raise ValueError('Can not convert %s type to str', type(date))
def get_user_summary(self, begin_date, end_date):
"""
获取用户增减数据
详情请参考
http://mp.weixin.qq.com/wiki/3/ecfed6e1a0a03b5f35e5efac98e864b7.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getusersummary',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
}
)
return res['list']
def get_user_cumulate(self, begin_date, end_date):
"""
获取累计用户数据
详情请参考
http://mp.weixin.qq.com/wiki/3/ecfed6e1a0a03b5f35e5efac98e864b7.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getusercumulate',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_interface_summary(self, begin_date, end_date):
"""
获取接口分析数据
详情请参考
http://mp.weixin.qq.com/wiki/8/30ed81ae38cf4f977194bf1a5db73668.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getinterfacesummary',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_interface_summary_hour(self, begin_date, end_date):
"""
获取接口分析分时数据
详情请参考
http://mp.weixin.qq.com/wiki/8/30ed81ae38cf4f977194bf1a5db73668.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getinterfacesummaryhour',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_article_summary(self, begin_date, end_date):
"""
获取图文群发每日数据
详情请参考
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getarticlesummary',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_article_total(self, begin_date, end_date):
"""
获取图文群发总数据
详情请参考
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getarticletotal',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_user_read(self, begin_date, end_date):
"""
获取图文统计数据
详情请参考
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getuserread',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_user_read_hour(self, begin_date, end_date):
"""
获取图文分时统计数据
详情请参考
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getuserreadhour',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_user_share(self, begin_date, end_date):
"""
获取图文分享转发数据
详情请参考
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getusershare',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_user_share_hour(self, begin_date, end_date):
"""
获取图文分享转发分时数据
详情请参考
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getusersharehour',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_upstream_msg(self, begin_date, end_date):
"""
获取消息发送概况数据
详情请参考
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getupstreammsg',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_upstream_msg_hour(self, begin_date, end_date):
"""
获取消息发送分时数据
详情请参考
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getupstreammsghour',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_upstream_msg_week(self, begin_date, end_date):
"""
获取消息发送周数据
详情请参考
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getupstreammsgweek',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_upstream_msg_month(self, begin_date, end_date):
"""
获取消息发送月数据
详情请参考
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getupstreammsgmonth',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_upstream_msg_dist(self, begin_date, end_date):
"""
获取消息发送分布数据
详情请参考
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getupstreammsgdist',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_upstream_msg_dist_week(self, begin_date, end_date):
"""
获取消息发送分布数据
详情请参考
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getupstreammsgdistweek',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res
def get_upstream_msg_dist_month(self, begin_date, end_date):
"""
获取消息发送分布数据
详情请参考
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
:param begin_date: 起始日期
:param end_date: 结束日期
:return: 统计数据列表
"""
res = self._post(
'getupstreammsgdistmonth',
data={
'begin_date': self._to_date_str(begin_date),
'end_date': self._to_date_str(end_date)
},
result_processor=lambda x: x['list']
)
return res

View File

@@ -1,284 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import base64
import urllib
from wechatpy.utils import to_text, to_binary
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatDevice(BaseWeChatAPI):
API_BASE_URL = 'https://api.weixin.qq.com/device/'
def send_message(self, device_type, device_id, user_id, content):
"""
主动发送消息给设备
详情请参考
http://iot.weixin.qq.com/document-2_3.html
:param device_type: 设备类型目前为“公众账号原始ID”
:param device_id: 设备ID
:param user_id: 微信用户账号的openid
:param content: 消息内容BASE64编码
:return: 返回的 JSON 数据包
"""
content = to_text(base64.b64encode(to_binary(content)))
return self._post(
'transmsg',
data={
'device_type': device_type,
'device_id': device_id,
'openid': user_id,
'content': content
}
)
def create_qrcode(self, device_ids):
"""
获取设备二维码
详情请参考
http://iot.weixin.qq.com/document-2_5.html
:param device_ids: 设备id的列表
:return: 返回的 JSON 数据包
"""
return self._post(
'create_qrcode',
data={
'device_num': len(device_ids),
'device_id_list': device_ids
}
)
def get_qrcode_url(self, ticket, data=None):
"""
通过 ticket 换取二维码地址
详情请参考
http://iot.weixin.qq.com/document-2_5.html
:param ticket: 二维码 ticket
:param data: 额外数据
:return: 二维码地址
"""
url = 'http://we.qq.com/d/{ticket}'.format(ticket=ticket)
if data:
if isinstance(data, (dict, tuple, list)):
data = urllib.urlencode(data)
data = to_text(base64.b64encode(to_binary(data)))
url = '{base}#{data}'.format(base=url, data=data)
return url
def bind(self, ticket, device_id, user_id):
"""
绑定设备
详情请参考
http://iot.weixin.qq.com/document-2_12.html
:param ticket: 绑定操作合法性的凭证由微信后台生成第三方H5通过客户端jsapi获得
:param device_id: 设备id
:param user_id: 用户对应的openid
:return: 返回的 JSON 数据包
"""
return self._post(
'bind',
data={
'ticket': ticket,
'device_id': device_id,
'openid': user_id
}
)
def unbind(self, ticket, device_id, user_id):
"""
解绑设备
详情请参考
http://iot.weixin.qq.com/document-2_12.html
:param ticket: 绑定操作合法性的凭证由微信后台生成第三方H5通过客户端jsapi获得
:param device_id: 设备id
:param user_id: 用户对应的openid
:return: 返回的 JSON 数据包
"""
return self._post(
'unbind',
data={
'ticket': ticket,
'device_id': device_id,
'openid': user_id
}
)
def compel_bind(self, device_id, user_id):
"""
强制绑定用户和设备
详情请参考
http://iot.weixin.qq.com/document-2_12.html
:param device_id: 设备id
:param user_id: 用户对应的openid
:return: 返回的 JSON 数据包
"""
return self._post(
'compel_bind',
data={
'device_id': device_id,
'openid': user_id
}
)
force_bind = compel_bind
def compel_unbind(self, device_id, user_id):
"""
强制解绑用户和设备
详情请参考
http://iot.weixin.qq.com/document-2_12.html
:param device_id: 设备id
:param user_id: 用户对应的openid
:return: 返回的 JSON 数据包
"""
return self._post(
'compel_unbind',
data={
'device_id': device_id,
'openid': user_id
}
)
force_unbind = compel_unbind
def get_stat(self, device_id):
"""
设备状态查询
详情请参考
http://iot.weixin.qq.com/document-2_7.html
:param device_id: 设备id
:return: 返回的 JSON 数据包
"""
return self._post(
'get_stat',
data={'device_id': device_id}
)
def verify_qrcode(self, ticket):
"""
验证二维码
详情请参考
http://iot.weixin.qq.com/document-2_9.html
:param ticket: 设备二维码的ticket
:return: 返回的 JSON 数据包
"""
return self._post(
'verify_qrcode',
data={'ticket': ticket}
)
def get_user_id(self, device_type, device_id):
"""
获取设备绑定openID
详情请参考
http://iot.weixin.qq.com/document-2_4.html
:param device_type: 设备类型目前为“公众账号原始ID”
:param device_id: 设备id
:return: 返回的 JSON 数据包
"""
return self._post(
'get_openid',
data={
'device_type': device_type,
'device_id': device_id
}
)
get_open_id = get_user_id
def get_binded_devices(self, user_id):
"""
通过openid获取用户在当前devicetype下绑定的deviceid列表
详情请参考
http://iot.weixin.qq.com/document-2_13.html
:param user_id: 要查询的用户的openid
:return: 返回的 JSON 数据包
"""
return self._post(
'get_bind_device',
data={'openid': user_id}
)
get_bind_device = get_binded_devices
def send_status_message(self, device_type, device_id, user_id, status):
"""
主动发送设备状态消息给微信终端
详情请参考
http://iot.weixin.qq.com/document-2_10.html
:param device_type: 设备类型目前为“公众账号原始ID”
:param device_id: 设备ID
:param user_id: 微信用户账号的openid
:param status: 设备状态0--未连接, 1--已连接
:return: 返回的 JSON 数据包
"""
return self._post(
'transmsg',
data={
'device_type': device_type,
'device_id': device_id,
'open_id': user_id,
'device_status': status
}
)
def authorize(self, devices, op_type=0):
"""
设备授权
详情请参考
http://iot.weixin.qq.com/document-2_6.html
:param devices: 设备信息的列表
:param op_type: 请求操作的类型限定取值为0设备授权 1设备更新
:return: 返回的 JSON 数据包
"""
return self._post(
'authorize',
data={
'device_num': len(devices),
'device_list': devices,
'op_type': op_type
}
)
def get_qrcode(self):
"""
获取deviceid和二维码
详情请参考
http://iot.weixin.qq.com/document-2_11.html
:return: 返回的 JSON 数据包
"""
return self._get('getqrcode')
def authorize_device(self, devices, op_type=1):
"""
设备授权
详情请参考
http://iot.weixin.qq.com/document-2_6.html
:param devices: 设备信息的列表
:param op_type: 请求操作的类型限定取值为0设备授权 1设备更新
:return: 返回的 JSON 数据包
"""
return self._post(
'authorize_device',
data={
'device_num': len(devices),
'device_list': devices,
'op_type': op_type
}
)

View File

@@ -1,148 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.utils import to_text
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatGroup(BaseWeChatAPI):
def create(self, name):
"""
创建分组
详情请参考
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
:param name: 分组名字30个字符以内
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.group.create('New Group')
"""
name = to_text(name)
return self._post(
'groups/create',
data={'group': {'name': name}}
)
def get(self, user_id=None):
"""
查询所有分组或查询用户所在分组 ID
详情请参考
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
:param user_id: 用户 ID提供时查询该用户所在分组否则查询所有分组
:return: 所有分组列表或用户所在分组 ID
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
group = client.group.get('openid')
"""
if user_id is None:
res = self._get(
'groups/get',
result_processor=lambda x: x['groups']
)
else:
res = self._post(
'groups/getid',
data={'openid': user_id},
result_processor=lambda x: x['groupid']
)
return res
def update(self, group_id, name):
"""
修改分组名
详情请参考
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
:param group_id: 分组id由微信分配
:param name: 分组名字30个字符以内
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.group.update(1234, 'New Name')
"""
name = to_text(name)
return self._post(
'groups/update',
data={
'group': {
'id': int(group_id),
'name': name
}
}
)
def move_user(self, user_id, group_id):
"""
移动用户分组
详情请参考
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
:param user_id: 用户 ID, 可以是单个或者列表,为列表时为批量移动用户分组
:param group_id: 分组 ID
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.group.move_user('openid', 1234)
"""
data = {'to_groupid': group_id}
if isinstance(user_id, (tuple, list)):
endpoint = 'groups/members/batchupdate'
data['openid_list'] = user_id
else:
endpoint = 'groups/members/update'
data['openid'] = user_id
return self._post(endpoint, data=data)
def delete(self, group_id):
"""
删除分组
详情请参考
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
:param group_id: 分组 ID
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.group.delete(1234)
"""
return self._post(
'groups/delete',
data={
'group': {
'id': group_id
}
}
)

View File

@@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
"""
wechatpy.client.jsapi
~~~~~~~~~~~~~~~~~~~~
This module provides some APIs for JS SDK
:copyright: (c) 2014 by messense.
:license: MIT, see LICENSE for more details.
"""
from __future__ import absolute_import, unicode_literals
import time
from wechatpy.utils import WeChatSigner
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatJSAPI(BaseWeChatAPI):
def get_ticket(self, type='jsapi'):
"""
获取微信 JS-SDK ticket
:return: 返回的 JSON 数据包
"""
return self._get(
'ticket/getticket',
params={'type': type}
)
def get_jsapi_ticket(self):
"""
获取微信 JS-SDK ticket
该方法会通过 session 对象自动缓存管理 ticket
:return: ticket
"""
ticket = self.session.get('jsapi_ticket')
expires_at = self.session.get('jsapi_ticket_expires_at', 0)
if not ticket or expires_at < int(time.time()):
jsapi_ticket = self.get_ticket('jsapi')
ticket = jsapi_ticket['ticket']
expires_at = int(time.time()) + int(jsapi_ticket['expires_in'])
self.session.set('jsapi_ticket', ticket)
self.session.set('jsapi_ticket_expires_at', expires_at)
return ticket
def get_jsapi_signature(self, noncestr, ticket, timestamp, url):
data = [
'noncestr={noncestr}'.format(noncestr=noncestr),
'jsapi_ticket={ticket}'.format(ticket=ticket),
'timestamp={timestamp}'.format(timestamp=timestamp),
'url={url}'.format(url=url),
]
signer = WeChatSigner(delimiter=b'&')
signer.add_data(*data)
return signer.signature

View File

@@ -1,167 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.utils import json
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatMaterial(BaseWeChatAPI):
def add_articles(self, articles):
"""
新增永久图文素材
详情请参考
http://mp.weixin.qq.com/wiki/14/7e6c03263063f4813141c3e17dd4350a.html
:param articles: 图文素材数组
:return: 返回的 JSON 数据包
"""
articles_data = []
for article in articles:
articles_data.append({
'thumb_media_id': article['thumb_media_id'],
'title': article['title'],
'content': article['content'],
'author': article.get('author', ''),
'content_source_url': article.get('content_source_url', ''),
'digest': article.get('digest', ''),
'show_cover_pic': article.get('show_cover_pic', '0')
})
return self._post(
'material/add_news',
data={
'articles': articles_data
}
)
def add(self, media_type, media_file, title=None, introduction=None):
"""
新增其它类型永久素材
详情请参考
http://mp.weixin.qq.com/wiki/14/7e6c03263063f4813141c3e17dd4350a.html
:param media_type: 媒体文件类型分别有图片image、语音voice、视频video和缩略图thumb
:param media_file: 要上传的文件,一个 File-object
:param title: 视频素材标题,仅上传视频素材时需要
:param introduction: 视频素材简介,仅上传视频素材时需要
:return: 返回的 JSON 数据包
"""
params = {
'access_token': self.access_token,
'type': media_type
}
if media_type == 'video':
assert title, 'Video title must be set'
assert introduction, 'Video introduction must be set'
description = {
'title': title,
'introduction': introduction
}
params['description'] = json.dumps(description)
return self._post(
'material/add_material',
params=params,
files={
'media': media_file
}
)
def get(self, media_id):
"""
获取永久素材
详情请参考
http://mp.weixin.qq.com/wiki/4/b3546879f07623cb30df9ca0e420a5d0.html
:param media_id: 素材的 media_id
:return: 图文素材返回图文列表,其它类型为素材的内容
"""
def _processor(res):
if isinstance(res, dict):
# 图文素材
return res.get('news_item', [])
return res
res = self._post(
'material/get_material',
data={
'media_id': media_id
},
result_processor=_processor
)
return res
def delete(self, media_id):
"""
删除永久素材
详情请参考
http://mp.weixin.qq.com/wiki/5/e66f61c303db51a6c0f90f46b15af5f5.html
:param media_id: 素材的 media_id
:return: 返回的 JSON 数据包
"""
return self._post(
'material/del_material',
data={
'media_id': media_id
}
)
def update_articles(self, media_id, index, articles):
"""
修改永久图文素材
详情请参考
http://mp.weixin.qq.com/wiki/4/19a59cba020d506e767360ca1be29450.html
:param media_id: 要修改的图文消息的 id
:param index: 要更新的文章在图文消息中的位置(多图文消息时,此字段才有意义),第一篇为 0
:param articles: 图文素材数组
:return: 返回的 JSON 数据包
"""
articles_data = []
for article in articles:
articles_data.append({
'thumb_media_id': article['thumb_media_id'],
'title': article['title'],
'content': article['content'],
'author': article.get('author', ''),
'content_source_url': article.get('content_source_url', ''),
'digest': article.get('digest', ''),
'show_cover_pic': article.get('show_cover_pic', '0')
})
return self._post(
'material/update_news',
data={
'media_id': media_id,
'index': index,
'articles': articles_data
}
)
def batchget(self, media_type, offset=0, count=20):
"""
批量获取永久素材列表
详情请参考
http://mp.weixin.qq.com/wiki/12/2108cd7aafff7f388f41f37efa710204.html
:param media_type: 媒体文件类型分别有图片image、语音voice、视频video和缩略图news
:param offset: 从全部素材的该偏移位置开始返回0 表示从第一个素材返回
:param count: 返回素材的数量取值在1到20之间
:return: 返回的 JSON 数据包
"""
return self._post(
'material/batchget_material',
data={
'type': media_type,
'offset': offset,
'count': count
}
)
def get_count(self):
"""
获取素材总数
详情请参考
http://mp.weixin.qq.com/wiki/16/8cc64f8c189674b421bee3ed403993b8.html
:return: 返回的 JSON 数据包
"""
return self._get('material/get_materialcount')

View File

@@ -1,126 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatMedia(BaseWeChatAPI):
def upload(self, media_type, media_file):
"""
上传临时素材
详情请参考
http://mp.weixin.qq.com/wiki/5/963fc70b80dc75483a271298a76a8d59.html
:param media_type: 媒体文件类型分别有图片image、语音voice、视频video和缩略图thumb
:param media_file: 要上传的文件,一个 File-object
:return: 返回的 JSON 数据包
"""
return self._post(
url='http://file.api.weixin.qq.com/cgi-bin/media/upload',
params={
'type': media_type
},
files={
'media': media_file
}
)
def download(self, media_id):
"""
获取临时素材
详情请参考
http://mp.weixin.qq.com/wiki/10/78b15308b053286e2a66b33f0f0f5fb6.html
:param media_id: 媒体文件 ID
:return: requests 的 Response 实例
"""
return self._get(
'http://file.api.weixin.qq.com/cgi-bin/media/get',
params={
'media_id': media_id
}
)
def get_url(self, media_id):
"""
获取临时素材下载地址
:param media_id: 媒体文件 ID
:return: 临时素材下载地址
"""
parts = (
'http://file.api.weixin.qq.com/cgi-bin/media/get',
'?access_token=',
self.access_token,
'&media_id=',
media_id
)
return ''.join(parts)
def upload_video(self, media_id, title, description):
"""
群发视频消息时获取视频 media_id
详情请参考
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
:param media_id: 需通过基础支持中的上传下载多媒体文件 :func:`upload` 来得到
:param title: 视频标题
:param description: 视频描述
:return: 返回的 JSON 数据包
"""
return self._post(
url='https://file.api.weixin.qq.com/cgi-bin/media/uploadvideo',
data={
'media_id': media_id,
'title': title,
'description': description
}
)
def upload_articles(self, articles):
"""
上传图文消息素材
详情请参考
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
:param articles: 图文消息数组
:return: 返回的 JSON 数据包
"""
articles_data = []
for article in articles:
articles_data.append({
'thumb_media_id': article['thumb_media_id'],
'title': article['title'],
'content': article['content'],
'author': article.get('author', ''),
'content_source_url': article.get('content_source_url', ''),
'digest': article.get('digest', ''),
'show_cover_pic': article.get('show_cover_pic', '0')
})
return self._post(
'media/uploadnews',
data={
'articles': articles_data
}
)
def upload_mass_image(self, media_file):
"""
上传群发消息内的图片
详情请参考
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
:param media_file: 要上传的文件,一个 File-object
:return: 上传成功时返回图片 URL
"""
res = self._post(
url='https://api.weixin.qq.com/cgi-bin/media/uploadimg',
files={
'media': media_file
},
result_processor=lambda x: x['url']
)
return res

View File

@@ -1,235 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.exceptions import WeChatClientException
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatMenu(BaseWeChatAPI):
def get(self):
"""
查询自定义菜单。
详情请参考
http://mp.weixin.qq.com/wiki/16/ff9b7b85220e1396ffa16794a9d95adc.html
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
menu = client.menu.get()
"""
try:
return self._get('menu/get')
except WeChatClientException as e:
if e.errcode == 46003:
# menu not exist
return None
else:
raise e
def create(self, menu_data):
"""
创建自定义菜单 ::
from wechatpy import WeChatClient
client = WeChatClient("appid", "secret")
client.menu.create({
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"type":"click",
"name":"歌手简介",
"key":"V1001_TODAY_SINGER"
},
{
"name":"菜单",
"sub_button":[
{
"type":"xml",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"xml",
"name":"视频",
"url":"http://v.qq.com/"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}
]
}
]
})
详情请参考
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013
:param menu_data: Python 字典
:return: 返回的 JSON 数据包
"""
return self._post(
'menu/create',
data=menu_data
)
update = create
def delete(self):
"""
删除自定义菜单。
详情请参考
http://mp.weixin.qq.com/wiki/16/8ed41ba931e4845844ad6d1eeb8060c8.html
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.menu.delete()
"""
return self._get('menu/delete')
def get_menu_info(self):
"""
获取自定义菜单配置
详情请参考
http://mp.weixin.qq.com/wiki/17/4dc4b0514fdad7a5fbbd477aa9aab5ed.html
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
menu_info = client.menu.get_menu_info()
"""
return self._get('get_current_selfmenu_info')
def add_conditional(self, menu_data):
"""
创建个性化菜单 ::
from wechatpy import WeChatClient
client = WeChatClient("appid", "secret")
client.menu.add_conditional({
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"type":"click",
"name":"歌手简介",
"key":"V1001_TODAY_SINGER"
},
{
"name":"菜单",
"sub_button":[
{
"type":"xml",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"xml",
"name":"视频",
"url":"http://v.qq.com/"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}
]
}
],
"matchrule":{
"group_id":"2",
"sex":"1",
"country":"中国",
"province":"广东",
"city":"广州",
"client_platform_type":"2"
}
})
详情请参考
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
:param menu_data: Python 字典
:return: 返回的 JSON 数据包
"""
return self._post(
'menu/addconditional',
data=menu_data
)
def del_conditional(self, menu_id):
"""
删除个性化菜单
详情请参考
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
:param menu_id: 菜单ID
:return: 返回的 JSON 数据包
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.menu.del_conditional('menu_id')
"""
return self._post(
'menu/delconditional',
data={'menuid': menu_id}
)
def try_match(self, user_id):
"""
测试个性化菜单匹配结果
详情请参考
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
:param user_id: 可以是粉丝的OpenID也可以是粉丝的微信号。
:return: 该接口将返回菜单配置
使用示例::
from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
res = client.menu.try_match('openid')
"""
return self._post(
'menu/trymatch',
data={'user_id': user_id}
)

View File

@@ -1,72 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.client.api.base import BaseWeChatAPI
from wechatpy.client.api.merchant.category import MerchantCategory
from wechatpy.client.api.merchant.stock import MerchantStock
from wechatpy.client.api.merchant.express import MerchantExpress
from wechatpy.client.api.merchant.group import MerchantGroup
from wechatpy.client.api.merchant.shelf import MerchantShelf
from wechatpy.client.api.merchant.order import MerchantOrder
from wechatpy.client.api.merchant.common import MerchantCommon
class WeChatMerchant(BaseWeChatAPI):
def __init__(self, *args, **kwargs):
super(WeChatMerchant, self).__init__(*args, **kwargs)
# sub APIs
self.category = MerchantCategory(self._client)
self.stock = MerchantStock(self._client)
self.express = MerchantExpress(self._client)
self.group = MerchantGroup(self._client)
self.shelf = MerchantShelf(self._client)
self.order = MerchantOrder(self._client)
self.common = MerchantCommon(self._client)
def create(self, product_data):
return self._post(
'merchant/create',
data=product_data
)
def delete(self, product_id):
return self._post(
'merchant/del',
data={
'product_id': product_id
}
)
def update(self, product_id, product_data):
product_data['product_id'] = product_id
return self._post(
'merchant/update',
data=product_data
)
def get(self, product_id):
return self._post(
'merchant/get',
data={
'product_id': product_id
}
)
def get_by_status(self, status):
return self._post(
'merchant/getbystatus',
data={
'status': status
}
)
def update_product_status(self, product_id, status):
return self._post(
'merchant/modproductstatus',
data={
'product_id': product_id,
'status': status
}
)

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from wechatpy.client.api.base import BaseWeChatAPI
class MerchantCategory(BaseWeChatAPI):
def get_sub_categories(self, cate_id):
res = self._post(
'merchant/category/getsub',
data={'cate_id': cate_id},
result_processor=lambda x: x['cate_list']
)
return res
def get_sku_list(self, cate_id):
res = self._post(
'merchant/category/getsku',
data={'cate_id': cate_id},
result_processor=lambda x: x['sku_table']
)
return res
def get_properties(self, cate_id):
res = self._post(
'merchant/category/getproperty',
data={'cate_id': cate_id},
result_processor=lambda x: x['properties']
)
return res

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