Merge branch 'develop' of https://e.coding.net/jikimo-hn/jikimo_sfs/jikimo_sf into feature/优化最新版返工
# Conflicts: # sf_manufacturing/views/mrp_workorder_view.xml
This commit is contained in:
@@ -5,7 +5,7 @@ import {patch} from '@web/core/utils/patch';
|
|||||||
import {_t} from "@web/core/l10n/translation";
|
import {_t} from "@web/core/l10n/translation";
|
||||||
import {FormStatusIndicator} from "@web/views/form/form_status_indicator/form_status_indicator";
|
import {FormStatusIndicator} from "@web/views/form/form_status_indicator/form_status_indicator";
|
||||||
import {ListRenderer} from "@web/views/list/list_renderer";
|
import {ListRenderer} from "@web/views/list/list_renderer";
|
||||||
import {StatusBarField} from "@web/views/fields/statusbar/statusbar_field";
|
// import {StatusBarField} from "@web/views/fields/statusbar/statusbar_field";
|
||||||
|
|
||||||
import {Field} from "@web/views/fields/field";
|
import {Field} from "@web/views/fields/field";
|
||||||
|
|
||||||
@@ -153,34 +153,34 @@ patch(ListRenderer.prototype, 'jikimo_frontend.ListRenderer', {
|
|||||||
|
|
||||||
|
|
||||||
// 根据进度条设置水印
|
// 根据进度条设置水印
|
||||||
const statusbar_params = {
|
// const statusbar_params = {
|
||||||
'已完工': 'bg-primary',
|
// '已完工': 'bg-primary',
|
||||||
'完成': 'bg-primary',
|
// '完成': 'bg-primary',
|
||||||
'采购订单': 'bg-primary',
|
// '采购订单': 'bg-primary',
|
||||||
'作废': 'bg-danger',
|
// '作废': 'bg-danger',
|
||||||
'封存(报废)': 'bg-danger',
|
// '封存(报废)': 'bg-danger',
|
||||||
}
|
// }
|
||||||
patch(StatusBarField.prototype, 'jikimo_frontend.StatusBarField', {
|
// patch(StatusBarField.prototype, 'jikimo_frontend.StatusBarField', {
|
||||||
setup() {
|
// setup() {
|
||||||
owl.onMounted(this.ribbons);
|
// owl.onMounted(this.ribbons);
|
||||||
return this._super(...arguments);
|
// return this._super(...arguments);
|
||||||
},
|
// },
|
||||||
ribbons() {
|
// ribbons() {
|
||||||
try {
|
// try {
|
||||||
const dom = $('.o_form_sheet.position-relative')
|
// const dom = $('.o_form_sheet.position-relative')
|
||||||
const status = statusbar_params[this.currentName]
|
// const status = statusbar_params[this.currentName]
|
||||||
if(status && dom.length) {
|
// if(status && dom.length) {
|
||||||
dom.prepend(`<div class="o_widget o_widget_web_ribbon">
|
// dom.prepend(`<div class="o_widget o_widget_web_ribbon">
|
||||||
<div class="ribbon ribbon-top-right">
|
// <div class="ribbon ribbon-top-right">
|
||||||
<span class="bg-opacity-75 ${status}" title="">${this.currentName}</span>
|
// <span class="bg-opacity-75 ${status}" title="">${this.currentName}</span>
|
||||||
</div>
|
// </div>
|
||||||
</div>`)
|
// </div>`)
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.log(e)
|
// console.log(e)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
document.addEventListener('click', function () {
|
document.addEventListener('click', function () {
|
||||||
|
|||||||
@@ -530,4 +530,11 @@ div:has(.o_required_modifier) > label::before {
|
|||||||
// 修复表格内容覆盖表头bug
|
// 修复表格内容覆盖表头bug
|
||||||
.o_list_renderer .o_list_table tbody th {
|
.o_list_renderer .o_list_table tbody th {
|
||||||
position: unset;
|
position: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修改表格下拉框会被表格下面数据框覆盖的bug
|
||||||
|
.tab-pane .o_field_widget {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
5
jikimo_system_order/__init__.py
Normal file
5
jikimo_system_order/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
39
jikimo_system_order/__manifest__.py
Normal file
39
jikimo_system_order/__manifest__.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': "jikimo_system_order",
|
||||||
|
|
||||||
|
'summary': """
|
||||||
|
系统工单""",
|
||||||
|
|
||||||
|
'description': """
|
||||||
|
用于处理针对系统的工作任务;
|
||||||
|
员工可以通过系统工单发起申请,由维护人员处理以后,填写处理结果。
|
||||||
|
""",
|
||||||
|
|
||||||
|
'author': "机企猫",
|
||||||
|
'website': "http://www.jikimo.com",
|
||||||
|
|
||||||
|
# Categories can be used to filter modules in modules listing
|
||||||
|
# Check https://github.com/odoo/odoo/blob/master/odoo/addons/base/module/module_data.xml
|
||||||
|
# for the full list
|
||||||
|
'category': 'Uncategorized',
|
||||||
|
'version': '0.1',
|
||||||
|
|
||||||
|
# any module necessary for this one to work correctly
|
||||||
|
'depends': ['base','mail'],
|
||||||
|
|
||||||
|
# always loaded
|
||||||
|
'data': [
|
||||||
|
'security/account_security.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'wizard/order_wizard.xml',
|
||||||
|
'views/notice_user_config.xml',
|
||||||
|
'views/yizuo_system_order_view.xml',
|
||||||
|
'views/work_order_number.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
],
|
||||||
|
# only loaded in demonstration mode
|
||||||
|
'demo': [
|
||||||
|
'demo/demo.xml',
|
||||||
|
],
|
||||||
|
}
|
||||||
3
jikimo_system_order/controllers/__init__.py
Normal file
3
jikimo_system_order/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
20
jikimo_system_order/controllers/controllers.py
Normal file
20
jikimo_system_order/controllers/controllers.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import http
|
||||||
|
|
||||||
|
# class TopSystemOrder(http.Controller):
|
||||||
|
# @http.route('/jikimo_system_order/jikimo_system_order/', auth='public')
|
||||||
|
# def index(self, **kw):
|
||||||
|
# return "Hello, world"
|
||||||
|
|
||||||
|
# @http.route('/jikimo_system_order/jikimo_system_order/objects/', auth='public')
|
||||||
|
# def list(self, **kw):
|
||||||
|
# return http.request.render('jikimo_system_order.listing', {
|
||||||
|
# 'root': '/jikimo_system_order/jikimo_system_order',
|
||||||
|
# 'objects': http.request.env['jikimo_system_order.jikimo_system_order'].search([]),
|
||||||
|
# })
|
||||||
|
|
||||||
|
# @http.route('/jikimo_system_order/jikimo_system_order/objects/<model("jikimo_system_order.jikimo_system_order"):obj>/', auth='public')
|
||||||
|
# def object(self, obj, **kw):
|
||||||
|
# return http.request.render('jikimo_system_order.object', {
|
||||||
|
# 'object': obj
|
||||||
|
# })
|
||||||
30
jikimo_system_order/demo/demo.xml
Normal file
30
jikimo_system_order/demo/demo.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- -->
|
||||||
|
<!-- <record id="object0" model="jikimo_system_order.jikimo_system_order"> -->
|
||||||
|
<!-- <field name="name">Object 0</field> -->
|
||||||
|
<!-- <field name="value">0</field> -->
|
||||||
|
<!-- </record> -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- <record id="object1" model="jikimo_system_order.jikimo_system_order"> -->
|
||||||
|
<!-- <field name="name">Object 1</field> -->
|
||||||
|
<!-- <field name="value">10</field> -->
|
||||||
|
<!-- </record> -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- <record id="object2" model="jikimo_system_order.jikimo_system_order"> -->
|
||||||
|
<!-- <field name="name">Object 2</field> -->
|
||||||
|
<!-- <field name="value">20</field> -->
|
||||||
|
<!-- </record> -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- <record id="object3" model="jikimo_system_order.jikimo_system_order"> -->
|
||||||
|
<!-- <field name="name">Object 3</field> -->
|
||||||
|
<!-- <field name="value">30</field> -->
|
||||||
|
<!-- </record> -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- <record id="object4" model="jikimo_system_order.jikimo_system_order"> -->
|
||||||
|
<!-- <field name="name">Object 4</field> -->
|
||||||
|
<!-- <field name="value">40</field> -->
|
||||||
|
<!-- </record> -->
|
||||||
|
<!-- -->
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
7
jikimo_system_order/models/__init__.py
Normal file
7
jikimo_system_order/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import constant
|
||||||
|
from . import order_classify
|
||||||
|
from . import system_work_order
|
||||||
|
from . import work_order_template
|
||||||
|
from . import res_config_setting
|
||||||
7
jikimo_system_order/models/constant.py
Normal file
7
jikimo_system_order/models/constant.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 工单状态
|
||||||
|
STATE_SELECTION = [('draft', u'草稿'), ('unconfirmed', u'待确认'), ('pending', u'待处理'),
|
||||||
|
('processed', u'已处理待评分'), ('completed', u'已完成'), ('closed', u'已关闭')]
|
||||||
|
|
||||||
|
GRADE = [('1', '1非常不满意'), ('2', '2不满意'), ('3', '3一般'), ('4', '4满意'), ('5', '5非常满意')]
|
||||||
25
jikimo_system_order/models/order_classify.py
Normal file
25
jikimo_system_order/models/order_classify.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class OrderClassify(models.Model):
|
||||||
|
_name = 'order.classify'
|
||||||
|
_order = 'sequence, name'
|
||||||
|
|
||||||
|
|
||||||
|
@api.constrains('name')
|
||||||
|
def check_base_name(self):
|
||||||
|
"""类型名称唯一"""
|
||||||
|
name_obj = self.env['order.classify'].search([('name', '=', self.name)])
|
||||||
|
if len(name_obj) >= 2:
|
||||||
|
raise ValidationError(u'该类型已存在')
|
||||||
|
|
||||||
|
# 名称
|
||||||
|
name = fields.Char(string=u'名称', size=20)
|
||||||
|
# 排序
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
# 是否有效
|
||||||
|
state = fields.Boolean(default=True, string='是否有效')
|
||||||
|
|
||||||
32
jikimo_system_order/models/res_config_setting.py
Normal file
32
jikimo_system_order/models/res_config_setting.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ResModelWeConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
lost_agent_id = fields.Char('企微通知应用ID')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_values(self):
|
||||||
|
"""
|
||||||
|
重载获取参数的方法,参数都存在系统参数中
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
values = super(ResModelWeConfigSettings, self).get_values()
|
||||||
|
config = self.env['ir.config_parameter'].sudo()
|
||||||
|
lost_agent_id = config.get_param('lost_agent_id', default='')
|
||||||
|
values.update(
|
||||||
|
lost_agent_id=lost_agent_id,
|
||||||
|
)
|
||||||
|
return values
|
||||||
|
|
||||||
|
def set_values(self):
|
||||||
|
super(ResModelWeConfigSettings, self).set_values()
|
||||||
|
ir_config = self.env['ir.config_parameter'].sudo()
|
||||||
|
ir_config.set_param("lost_agent_id", self.lost_agent_id or "")
|
||||||
|
|
||||||
183
jikimo_system_order/models/system_work_order.py
Normal file
183
jikimo_system_order/models/system_work_order.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo import exceptions
|
||||||
|
from .constant import STATE_SELECTION, GRADE
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class SystemWorkOrder(models.Model):
|
||||||
|
_name = 'system.work.order'
|
||||||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
_order = 'date desc'
|
||||||
|
_description = u'系统工单'
|
||||||
|
_rec_name = 'order_number'
|
||||||
|
|
||||||
|
def get_is_technicist(self):
|
||||||
|
self._cr.execute(
|
||||||
|
"select u.id from res_users u left join res_groups_users_rel r on r.uid = u.id where r.gid in (select g.id from res_groups g where g.name = '技术员权限') and u.id ='%s'",
|
||||||
|
(self.env.user.id,))
|
||||||
|
hr = self._cr.dictfetchall()
|
||||||
|
if len(hr) > 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# def get_user_department_id(self):
|
||||||
|
# """根据用户id系统员工id"""
|
||||||
|
# employee = self.env['hr.employee'].sudo().search([('user_id', '=', self.env.uid)], limit=1)
|
||||||
|
# if employee:
|
||||||
|
# if len(employee) > 0:
|
||||||
|
# if not employee.department_id:
|
||||||
|
# raise exceptions.Warning(u'您当前使用的用户没有所属部门')
|
||||||
|
# return employee.department_id
|
||||||
|
# else:
|
||||||
|
# return False
|
||||||
|
# else:
|
||||||
|
# raise exceptions.Warning(u'您当前使用的用户没有关联员工')
|
||||||
|
|
||||||
|
@api.onchange('order_template_id')
|
||||||
|
def get_title(self):
|
||||||
|
"""选择模板自动填充"""
|
||||||
|
if self.order_template_id:
|
||||||
|
self.title = self.order_template_id.title_template
|
||||||
|
self.text = self.order_template_id.text_template
|
||||||
|
|
||||||
|
# 工单编号
|
||||||
|
order_number = fields.Char(string=u'工单编号', default='/')
|
||||||
|
# 紧急程度
|
||||||
|
urgency_degree = fields.Selection([('0', u'0星'), ('1', u'一星'), ('2', u'二星'), ('3', u'三星'), ('4', u'四星'),
|
||||||
|
('5', u'五星')], string=u'紧急程度', help='五星为最紧急!', default='5')
|
||||||
|
# 工单分类(可以配置,并调整优先级)
|
||||||
|
order_type = fields.Many2one('order.classify', string=u'工单分类', domain=[('state', '=', True)])
|
||||||
|
# 发起人所属公司(res.company)
|
||||||
|
initiator_company_id = fields.Many2one('res.company', string=u'发起人所属公司', default=lambda self: self.env.user.company_id)
|
||||||
|
# 发起人部门(hr.department)
|
||||||
|
# initiator_department_id = fields.Many2one('hr.department', string=u'发起人部门', default=get_user_department_id)
|
||||||
|
# 发起人(hr.employee)
|
||||||
|
initiator_id = fields.Many2one('res.users', string=u'发起人', default=lambda self: self.env.user)
|
||||||
|
# 发起时间
|
||||||
|
date = fields.Datetime(string=u'发起时间', default=lambda self: fields.datetime.now())
|
||||||
|
# 确认人
|
||||||
|
confirm_id = fields.Many2one('res.users', string=u'确认人')
|
||||||
|
# 确认日期
|
||||||
|
confirmation_date = fields.Datetime(string=u'确认时间')
|
||||||
|
# 模板
|
||||||
|
order_template_id = fields.Many2one('work.order.template', string=u'模板', domain=[('state', '=', True)])
|
||||||
|
# 标题
|
||||||
|
title = fields.Char(string=u'标题')
|
||||||
|
# 正文
|
||||||
|
text = fields.Html(string=u'正文')
|
||||||
|
# 状态[草稿\待确认\待处理\已处理\已关闭]
|
||||||
|
state = fields.Selection(STATE_SELECTION, default='draft', string=u'状态')
|
||||||
|
# 关闭原因
|
||||||
|
close_cause = fields.Text(string=u'关闭问题原因')
|
||||||
|
# 关闭时间
|
||||||
|
close_time = fields.Datetime(string=u'关闭问题时间')
|
||||||
|
# 关闭人
|
||||||
|
close_user_id = fields.Many2one('res.users', string=u'关闭人')
|
||||||
|
# 解决人
|
||||||
|
solve_people_id = fields.Many2one('res.users', string=u'解决人')
|
||||||
|
# 用户实际问题
|
||||||
|
users_problem = fields.Text(string=u'用户实际问题')
|
||||||
|
# 最终解决方案
|
||||||
|
solution = fields.Text(string=u'最终解决方案')
|
||||||
|
# 判断是否为技术人员
|
||||||
|
# is_technicist = fields.Boolean(string=u'是否为技术人员', default=get_is_technicist)
|
||||||
|
# 打分
|
||||||
|
grade = fields.Selection(GRADE, string=u'评分')
|
||||||
|
# 评价按钮的显示
|
||||||
|
is_display = fields.Boolean('控制显示评价按钮', compute='compute_is_display')
|
||||||
|
|
||||||
|
def compute_is_display(self):
|
||||||
|
for item in self:
|
||||||
|
if item.state == 'processed' and self.env.user.id == item.initiator_id.id:
|
||||||
|
item.is_display = True
|
||||||
|
else:
|
||||||
|
item.is_display = False
|
||||||
|
|
||||||
|
@api.onchange('order_type')
|
||||||
|
def _onchange_order_type(self):
|
||||||
|
self.order_template_id = None
|
||||||
|
self.title = None
|
||||||
|
self.text = None
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
# 创建编号
|
||||||
|
if vals.get('order_number', '/') == '/':
|
||||||
|
vals['order_number'] = self.env['ir.sequence'].get('system.work.order') or '/'
|
||||||
|
return super(SystemWorkOrder, self).create(vals)
|
||||||
|
|
||||||
|
def do_draft(self, order=None):
|
||||||
|
"""状态草稿"""
|
||||||
|
bill = self
|
||||||
|
if order:
|
||||||
|
bill = order
|
||||||
|
if bill.state == 'unconfirmed':
|
||||||
|
state_remark = u'待确认 --> 草稿'
|
||||||
|
# bill.message_post(u'操作人:%s,操作时间:%s,状态变更过程:%s' % (self.env.user.name,
|
||||||
|
# (datetime.datetime.now() + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), state_remark))
|
||||||
|
bill.state = 'draft'
|
||||||
|
|
||||||
|
def do_unconfirmed(self):
|
||||||
|
"""状态待确认"""
|
||||||
|
if self.state == 'draft':
|
||||||
|
state_remark = u'草稿 --> 待确认'
|
||||||
|
# self.message_post(u'操作人:%s,操作时间:%s,状态变更过程:%s' % (
|
||||||
|
# self.env.user.name,
|
||||||
|
# (datetime.datetime.now() + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), state_remark))
|
||||||
|
self.state = 'unconfirmed'
|
||||||
|
# 获取通知人
|
||||||
|
objs = self.env['system.order.notice'].search([])
|
||||||
|
user_ids = objs.notice_user_ids.filtered(lambda item: item.we_employee_id not in ['', False])
|
||||||
|
we_employee_ids = user_ids.mapped('we_employee_id')
|
||||||
|
lost_agent_id = self.env['ir.config_parameter'].sudo().get_param('lost_agent_id')
|
||||||
|
wechat = self.env['we.config'].sudo().get_wechat(agent_id=lost_agent_id)
|
||||||
|
# agent_id, user_ids, content
|
||||||
|
content = """您有一张工单<font color=\"warning\">待处理</font>:**工单标题:{2}**
|
||||||
|
>创建人:{1}
|
||||||
|
>提交时间:{3}
|
||||||
|
>紧急程度:{0}星
|
||||||
|
请查看工单消息,并及时处理!
|
||||||
|
""".format(self.urgency_degree,
|
||||||
|
self.initiator_id.name, self.title, (self.date + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M'))
|
||||||
|
for we_employee_id in we_employee_ids:
|
||||||
|
try:
|
||||||
|
wechat.message.send_markdown(agent_id=lost_agent_id, user_ids=we_employee_id, content=content)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('工单处理发送消息异常%s' % str(e))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_pending(self):
|
||||||
|
"""状态待处理"""
|
||||||
|
if self.state == 'unconfirmed':
|
||||||
|
state_remark = u'待确认 --> 待处理'
|
||||||
|
# self.message_post(u'操作人:%s,操作时间:%s,状态变更过程:%s' % (
|
||||||
|
# self.env.user.name,
|
||||||
|
# (datetime.datetime.now() + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), state_remark))
|
||||||
|
self.state = 'pending'
|
||||||
|
self.confirm_id = self.env.user
|
||||||
|
self.confirmation_date = fields.datetime.now()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def urned_off(self):
|
||||||
|
"""状态关闭"""
|
||||||
|
if self.close_cause:
|
||||||
|
self.state = 'closed'
|
||||||
|
self.close_time = fields.datetime.now()
|
||||||
|
else:
|
||||||
|
raise ValidationError(u'请注明关闭原因')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
for item in self:
|
||||||
|
if item.state != "draft":
|
||||||
|
raise ValidationError(u'只能删除状态为【草稿】的工单。')
|
||||||
|
elif item.env.uid != item.initiator_id.id:
|
||||||
|
raise ValidationError(u'非本人不能删除')
|
||||||
|
else:
|
||||||
|
super(SystemWorkOrder, item).unlink()
|
||||||
38
jikimo_system_order/models/work_order_template.py
Normal file
38
jikimo_system_order/models/work_order_template.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
|
class WorkOrderTemplate(models.Model):
|
||||||
|
_name = 'work.order.template'
|
||||||
|
_order = 'num'
|
||||||
|
|
||||||
|
# 编号
|
||||||
|
num = fields.Char(string=u'编号', default='/')
|
||||||
|
# 名称
|
||||||
|
name = fields.Char(string=u'模板名称', required="1")
|
||||||
|
# 分类
|
||||||
|
work_order_type = fields.Many2one('order.classify', string=u'系统工单分类', domain=[('state', '=', True)])
|
||||||
|
# 模板标题
|
||||||
|
title_template = fields.Char(string=u'模板标题')
|
||||||
|
# 模板正文
|
||||||
|
text_template = fields.Html(string=u'模板正文')
|
||||||
|
# 模板说明
|
||||||
|
template_explain = fields.Text(string=u'模板说明')
|
||||||
|
# 是否有效
|
||||||
|
state = fields.Boolean(default=True, string=u'是否有效')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
# 创建编号
|
||||||
|
if vals.get('num', '/') == '/':
|
||||||
|
vals['num'] = self.env['ir.sequence'].get('work.order.template') or '/'
|
||||||
|
return super(WorkOrderTemplate, self).create(vals)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemOrderNotice(models.Model):
|
||||||
|
_name = 'system.order.notice'
|
||||||
|
_description = '工单处理人设置'
|
||||||
|
|
||||||
|
notice_user_ids = fields.Many2many('res.users', string='工单处理人')
|
||||||
|
|
||||||
24
jikimo_system_order/security/account_security.xml
Normal file
24
jikimo_system_order/security/account_security.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0"> <!-- noupdate表示,当模块升级时是否更新本条数据-->
|
||||||
|
<!--运维权限组-->
|
||||||
|
<record id="group_operations_permissions_rwc" model="res.groups">
|
||||||
|
<field name="name">运维权限</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="system_order_user_rule" model="ir.rule">
|
||||||
|
<field name="name">用户访问工单信息</field>
|
||||||
|
<field name="model_id" ref="model_system_work_order"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="domain_force">[('initiator_id', '=', user.id)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="system_order_group_operations_rule" model="ir.rule">
|
||||||
|
<field name="name">运维访问工单信息</field>
|
||||||
|
<field name="model_id" ref="model_system_work_order"/>
|
||||||
|
<field name="groups" eval="[(4, ref('jikimo_system_order.group_operations_permissions_rwc'))]"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
16
jikimo_system_order/security/ir.model.access.csv
Normal file
16
jikimo_system_order/security/ir.model.access.csv
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
|
||||||
|
inside_system_order_classify_r,jikimo_system_order.order_classify,model_order_classify,,1,1,1,1
|
||||||
|
inside_system_work_order_rc,jikimo_system_order.system_work_order,model_system_work_order,,1,1,1,1
|
||||||
|
inside_work_order_template_r,jikimo_system_order.work_order_template,model_work_order_template,,1,1,1,1
|
||||||
|
|
||||||
|
inside_system_order_classify_rwc,jikimo_system_order.order_classify,model_order_classify,group_operations_permissions_rwc,1,1,1,0
|
||||||
|
inside_system_work_order_rwc,jikimo_system_order.system_work_order,model_system_work_order,group_operations_permissions_rwc,1,1,1,0
|
||||||
|
inside_work_order_template_rwc,jikimo_system_order.work_order_template,model_work_order_template,group_operations_permissions_rwc,1,1,1,0
|
||||||
|
|
||||||
|
order_close_wizard_group_user,jikimo_system_order.order_close_wizard,model_order_close_wizard,base.group_user,1,1,1,1
|
||||||
|
order_other_wizard_group_user,jikimo_system_order.order_other_wizard,model_order_other_wizard,base.group_user,1,1,1,1
|
||||||
|
order_technician_wizard_group_user,jikimo_system_order.order_technician_wizard,model_order_technician_wizard,base.group_user,1,1,1,1
|
||||||
|
system_work_order_wizard_group_user,jikimo_system_order.system_work_order_wizard,model_system_work_order_wizard,base.group_user,1,1,1,1
|
||||||
|
|
||||||
|
system_order_notice_group_user,jikimo_system_order.system_order_notice,model_system_order_notice,base.group_user,1,1,1,1
|
||||||
|
BIN
jikimo_system_order/static/description/icon.png
Normal file
BIN
jikimo_system_order/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
58
jikimo_system_order/views/notice_user_config.xml
Normal file
58
jikimo_system_order/views/notice_user_config.xml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
# ---------- 工单通知处理人设置 ------------
|
||||||
|
|
||||||
|
<record model="ir.ui.view" id="tree_system_order_notice_view">
|
||||||
|
<field name="name">tree.system.order.notice</field>
|
||||||
|
<field name="model">system.order.notice</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="工单处理人设置" editable="top">
|
||||||
|
<field name="notice_user_ids" widget="many2many_tags" required="1" options="{'no_create': True, 'no_edit': True}"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record model="ir.ui.view" id="search_system_order_notice_view">
|
||||||
|
<field name="name">search.system.order.notice</field>
|
||||||
|
<field name="model">system.order.notice</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="工单处理人设置">
|
||||||
|
<field name="notice_user_ids" string="模糊搜索"
|
||||||
|
filter_domain="[('notice_user_ids', 'ilike', self)]"/>
|
||||||
|
<separator></separator>
|
||||||
|
|
||||||
|
<field name="notice_user_ids" string="处理人"/>
|
||||||
|
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record model="ir.actions.act_window" id="action_system_order_notice_view">
|
||||||
|
<field name="name">工单处理人</field>
|
||||||
|
<field name="res_model">system.order.notice</field>
|
||||||
|
<field name="view_mode">tree</field>
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
<field name="context">{}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
[工单处理人] 还没有哦!点左上角的[创建]按钮,沙发归你了!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
28
jikimo_system_order/views/res_config_settings_views.xml
Normal file
28
jikimo_system_order/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="res_config_settings_we_view_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.we.view.form.inherit.bpm</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[hasclass('app_settings_block')]/div[6]" position="after">
|
||||||
|
<div>
|
||||||
|
<h2>企微通知应用ID</h2>
|
||||||
|
<div class="row mt16 o_settings_container" id="jd_api">
|
||||||
|
<div class="col-12 col-lg-6 o_setting_box">
|
||||||
|
<div class="o_setting_left_pane"/>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<div class="text-muted">
|
||||||
|
<label for="lost_agent_id"/>
|
||||||
|
<field name="lost_agent_id"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
23
jikimo_system_order/views/work_order_number.xml
Normal file
23
jikimo_system_order/views/work_order_number.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="True">
|
||||||
|
<!-- 工单流水号 -->
|
||||||
|
<record id="seq_work_order" model="ir.sequence">
|
||||||
|
<field name="name">seq_work_order</field>
|
||||||
|
<field name="company_id"/>
|
||||||
|
<field name="code">system.work.order</field>
|
||||||
|
<field name="prefix">SO%(year)s%(month)s%(day)s</field>
|
||||||
|
<field name="padding">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 模板编号 -->
|
||||||
|
<record id="seq_order_template" model="ir.sequence">
|
||||||
|
<field name="name">seq_order_template</field>
|
||||||
|
<field name="company_id"/>
|
||||||
|
<field name="code">work.order.template</field>
|
||||||
|
<field name="prefix">TL</field>
|
||||||
|
<field name="padding">1</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
243
jikimo_system_order/views/yizuo_system_order_view.xml
Normal file
243
jikimo_system_order/views/yizuo_system_order_view.xml
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!--工单信息-->
|
||||||
|
<record model="ir.ui.view" id="work_order_tree">
|
||||||
|
<field name="name">工单信息</field>
|
||||||
|
<field name="model">system.work.order</field><!--对应表单名称-->
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="state" widget="badge" decoration-primary="state == 'draft'"
|
||||||
|
decoration-success="state in ('processed', 'completed')"
|
||||||
|
decoration-danger="state == 'pending'" decoration-warning="state in ('unconfirmed')"/>
|
||||||
|
<field name="order_number"/>
|
||||||
|
<field name="title"/>
|
||||||
|
<field name="initiator_id"/>
|
||||||
|
<field name="date"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!--新建系统工单-->
|
||||||
|
<record model="ir.ui.view" id="ork_order_form">
|
||||||
|
<field name="name">新建系统工单</field>
|
||||||
|
<field name="model">system.work.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<header>
|
||||||
|
<field name="is_display" invisible="1"/>
|
||||||
|
<button string='提交' class="oe_highlight" states="draft"
|
||||||
|
type="object" name="do_unconfirmed"
|
||||||
|
attrs="{'invisible': [('state', '!=', 'draft')]}"/>
|
||||||
|
<button string='追回编辑' states="unconfirmed"
|
||||||
|
type="action" name="%(system_work_order_wizard_view_act_window)d"
|
||||||
|
context="{'explain':'确认要执行此操作吗?','object_name':'system.work.order','function_name':'do_draft','object_id':id}"/>
|
||||||
|
|
||||||
|
<button name="do_pending" states="unconfirmed"
|
||||||
|
string="确认可处理" type="object" class="oe_highlight"
|
||||||
|
groups="jikimo_system_order.group_operations_permissions_rwc"/>
|
||||||
|
|
||||||
|
<button string='处理工单' class="oe_highlight" states="pending"
|
||||||
|
type="action" name="%(launch_order_technician_wizard)d"
|
||||||
|
groups="jikimo_system_order.group_operations_permissions_rwc"/>
|
||||||
|
<button string='评价' class="oe_highlight" attrs="{'invisible': [('is_display', '=', False)]}"
|
||||||
|
type="action" name="%(launch_order_other_wizard)d" context="{'active_id':id}"/>
|
||||||
|
<button name="%(launch_order_close_wizard)d" string="关闭该工单"
|
||||||
|
attrs="{'invisible': ['|',('state', '=', 'draft'),'|',('state','=','completed'),('state','=','closed')]}"
|
||||||
|
type="action" context="{'active_id':id}"/>
|
||||||
|
|
||||||
|
<field name="state" widget="statusbar"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<!-- <label for="order_number" class="oe_edit_only"/>-->
|
||||||
|
<group>
|
||||||
|
<field name="order_number" required="True" readonly="1"/>
|
||||||
|
<field name="order_type" required="True" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_create': True}"/>
|
||||||
|
<field name="date" required="True" readonly="True"/>
|
||||||
|
<field name="order_template_id" attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||||
|
domain="[('work_order_type','=',order_type),('state','=',True)]" options="{'no_create': True}"/>
|
||||||
|
<field name="confirmation_date" readonly="True"/>
|
||||||
|
<field name="urgency_degree" required="True" attrs="{'readonly': [('state','!=','draft')]}" widget="priority"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="initiator_company_id" required="True" readonly="True"/>
|
||||||
|
<!-- <field name="initiator_department_id" required="True" readonly="True"/>-->
|
||||||
|
<field name="initiator_id" required="True" readonly="True"/>
|
||||||
|
<field name="confirm_id" readonly="True"/>
|
||||||
|
<field name="solve_people_id" readonly="True"/>
|
||||||
|
<field name="close_user_id" readonly="True"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="title" attrs="{'readonly': [('state', '!=', 'draft')]}" required="True"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="工单内容">
|
||||||
|
<field name="text" attrs="{'readonly': [('state','!=','draft')]}" required="True"/>
|
||||||
|
</page>
|
||||||
|
<page string="解决方案">
|
||||||
|
<group>
|
||||||
|
<field name="users_problem" readonly="True"/>
|
||||||
|
<field name="solution" readonly="True"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="其他">
|
||||||
|
<group>
|
||||||
|
<field name="close_cause" readonly="True"/>
|
||||||
|
<field name="close_time" readonly="True"/>
|
||||||
|
<field name="grade" readonly="True"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<!-- <div class="oe_chatter">-->
|
||||||
|
<!-- <field name="message_follower_ids" widget="mail_followers"/>-->
|
||||||
|
<!-- <field name="message_ids" widget="mail_thread"/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 搜索工单 -->
|
||||||
|
<record model="ir.ui.view" id="restaurant_search">
|
||||||
|
<field name="name">搜索工单</field>
|
||||||
|
<field name="model">system.work.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field string='发起人' name="initiator_id" widget="char" required="True"/>
|
||||||
|
<field string='标题' name="title" widget="char"/>
|
||||||
|
<field string='正文' name="text" widget="html"/>
|
||||||
|
<field string='实际问题' name="users_problem" widget="text"/>
|
||||||
|
<field string='解决方案' name="solution" widget="text"/>
|
||||||
|
<filter name="today" string="今日工单" domain="[('date','=',time.strftime('%%Y-%%m-%%d'))]"/>
|
||||||
|
<filter name="yesterday" string="昨日工单"
|
||||||
|
domain="[('date', '=', (context_today() - relativedelta(days=1)).strftime('%Y-%m-%d'))]"/>
|
||||||
|
<filter name="month" string="本月工单"
|
||||||
|
domain="[('date','>=', time.strftime('%Y-%m-01')),('date','<', (context_today() + relativedelta(months=1)).strftime('%Y-%m-01'))]"/>
|
||||||
|
<filter name="last_month" string="上月工单"
|
||||||
|
domain="[('date','<', time.strftime('%Y-%m-01')),('date','>=', (context_today() - relativedelta(months=1)).strftime('%Y-%m-01'))]"/>
|
||||||
|
<filter name="unconfirmed" string="待确认" domain="[('state','=','unconfirmed')]"/>
|
||||||
|
<filter name="pending" string="待处理" domain="[('state','=','pending')]"/>
|
||||||
|
<filter name="processed" string="已处理"
|
||||||
|
domain="['|', ('state','=','processed'), ('state','=','closed')]"/>
|
||||||
|
<group>
|
||||||
|
<filter string='发起人' name="initiator_id" context='{"group_by":"initiator_id"}'/>
|
||||||
|
<filter string='工单分类' name="order_type" context='{"group_by":"order_type"}'/>
|
||||||
|
<filter string='模板' name="order_template_id" context='{"group_by":"order_template_id"}'/>
|
||||||
|
<filter string='状态' name="state" context='{"group_by":"state"}'/>
|
||||||
|
<filter string='紧急情况' name="state" context='{"group_by":"urgency_degree"}'/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.ui.view" id="graph_tree">
|
||||||
|
<field name="name">工单图表</field>
|
||||||
|
<field name="model">system.work.order</field><!--对应表单名称-->
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot>
|
||||||
|
<field name="date" type="row" interval="day"/>
|
||||||
|
<field name="order_type" type="col"/>
|
||||||
|
<field name="state" type="row"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 工单 -->
|
||||||
|
<record model="ir.actions.act_window" id="system_order">
|
||||||
|
<field name="name">工单</field>
|
||||||
|
<field name="res_model">system.work.order</field>
|
||||||
|
<field name="view_mode">tree,form,search,graph,pivot</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!--工单模板信息-->
|
||||||
|
<record model="ir.ui.view" id="order_template_tree">
|
||||||
|
<field name="name">工单模板信息</field>
|
||||||
|
<field name="model">work.order.template</field><!--对应表单名称-->
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="num"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="work_order_type"/>
|
||||||
|
<field name="title_template"/>
|
||||||
|
<field name="template_explain"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!--新建系统工单模板-->
|
||||||
|
<record model="ir.ui.view" id="order_template_form">
|
||||||
|
<field name="name">新建系统工单模板</field>
|
||||||
|
<field name="model">work.order.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="num" required="True" readonly="True"/>
|
||||||
|
<field name="name" required="True"/>
|
||||||
|
<field name="work_order_type" required="True"/>
|
||||||
|
<field name="template_explain" required="True" style="height: 50px;"/>
|
||||||
|
<field name="title_template" required="True"/>
|
||||||
|
<field name="state"/>
|
||||||
|
<field name="text_template" required="True"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 工单模板 -->
|
||||||
|
<record model="ir.actions.act_window" id="work_template">
|
||||||
|
<field name="name">工单模板</field>
|
||||||
|
<field name="res_model">work.order.template</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!--工单分类信息-->
|
||||||
|
<record model="ir.ui.view" id="order_type_tree">
|
||||||
|
<field name="name">工单分类信息</field>
|
||||||
|
<field name="model">order.classify</field><!--对应表单名称-->
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!--新建系统分类信息-->
|
||||||
|
<record model="ir.ui.view" id="order_type_form">
|
||||||
|
<field name="name">新建系统分类信息</field>
|
||||||
|
<field name="model">order.classify</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name" required="True"/>
|
||||||
|
<field name="sequence" invisible="True"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 工单分类 -->
|
||||||
|
<record model="ir.actions.act_window" id="classify">
|
||||||
|
<field name="name">工单分类</field>
|
||||||
|
<field name="res_model">order.classify</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<menuitem name="系统工单" id="work_order_1_list" web_icon="jikimo_system_order,static/description/icon.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"/>
|
||||||
|
<menuitem name="工单设置" id="system_order_notice_user_config" parent="work_order_1_list" action="action_system_order_notice_view" groups="jikimo_system_order.group_operations_permissions_rwc"/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
6
jikimo_system_order/wizard/__init__.py
Normal file
6
jikimo_system_order/wizard/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import order_other_wizard
|
||||||
|
from . import order_technician_wizard
|
||||||
|
from . import order_close_wizard
|
||||||
|
from . import system_work_order_wizard
|
||||||
79
jikimo_system_order/wizard/order_close_wizard.py
Normal file
79
jikimo_system_order/wizard/order_close_wizard.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.addons.jikimo_system_order.models.constant import STATE_SELECTION
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
import datetime, logging
|
||||||
|
|
||||||
|
|
||||||
|
class OrderCloseWizard(models.TransientModel):
|
||||||
|
_name = 'order.close.wizard'
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
if self._context.get('active_id'):
|
||||||
|
obj = self.env['system.work.order'].browse(self._context.get('active_id'))
|
||||||
|
if obj.initiator_id.id != self.env.user.id:
|
||||||
|
raise ValidationError(u'非本人无法操作')
|
||||||
|
return obj
|
||||||
|
|
||||||
|
order_id = fields.Many2one('system.work.order', string=u'工单ID',
|
||||||
|
default=lambda self: self.get_context().id)
|
||||||
|
# 关闭原因
|
||||||
|
close_cause = fields.Text(string=u'关闭问题原因', default=lambda self: self.get_context().close_cause)
|
||||||
|
# 关闭时间
|
||||||
|
close_time = fields.Datetime(string=u'关闭问题时间', default=fields.datetime.now())
|
||||||
|
# 状态
|
||||||
|
state = fields.Selection(STATE_SELECTION, default='closed', string=u'状态')
|
||||||
|
# 关闭人
|
||||||
|
close_user_id = fields.Many2one('res.users', string=u'关闭人', default=lambda self: self.env.user)
|
||||||
|
|
||||||
|
|
||||||
|
def sure(self):
|
||||||
|
self.order_id.close_cause = self.close_cause
|
||||||
|
self.order_id.close_time = self.close_time
|
||||||
|
if self.order_id.state == 'unconfirmed':
|
||||||
|
state_remark = u'待确认 --> 已关闭'
|
||||||
|
if self.order_id.state == 'pending':
|
||||||
|
state_remark = u'待处理 --> 已关闭'
|
||||||
|
if self.order_id.state == 'processed':
|
||||||
|
state_remark = u'已处理待评分 --> 已关闭'
|
||||||
|
# self.order_id.message_post(u'操作人:%s,操作时间:%s,状态变更过程:%s' % (
|
||||||
|
# self.env.user.name,
|
||||||
|
# (datetime.datetime.now() + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), state_remark))
|
||||||
|
self.order_id.state = self.state
|
||||||
|
self.order_id.close_user_id = self.close_user_id
|
||||||
|
we_employee_ids = []
|
||||||
|
if self.order_id.initiator_id.we_employee_id:
|
||||||
|
we_employee_ids.append(self.order_id.initiator_id.we_employee_id)
|
||||||
|
lost_agent_id = self.env['ir.config_parameter'].sudo().get_param('lost_agent_id')
|
||||||
|
wechat = self.env['we.config'].sudo().get_wechat(agent_id=lost_agent_id)
|
||||||
|
# agent_id, user_ids, content
|
||||||
|
content = """您提交的工单-**工单标题:{0}**-<font color=\"#FF0000\">**已关闭**</font>
|
||||||
|
>提交时间:{1}
|
||||||
|
>处理时间:{2}
|
||||||
|
>处理人:{3}
|
||||||
|
如有问题,请联系系统管理员!
|
||||||
|
""".format(self.order_id.title,
|
||||||
|
(self.order_id.date + datetime.timedelta(hours=8)).strftime(
|
||||||
|
'%Y-%m-%d %H:%M'), (datetime.datetime.now() + datetime.timedelta(
|
||||||
|
hours=8)).strftime('%Y-%m-%d %H:%M'), self.env.user.name or '')
|
||||||
|
# wechat.message.send_markdown(agent_id=lost_agent_id, user_ids=we_employee_ids, content=content)
|
||||||
|
for we_employee_id in we_employee_ids:
|
||||||
|
try:
|
||||||
|
wechat.message.send_markdown(agent_id=lost_agent_id, user_ids=we_employee_id, content=content)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('工单关闭发送消息异常%s' % str(e))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
42
jikimo_system_order/wizard/order_other_wizard.py
Normal file
42
jikimo_system_order/wizard/order_other_wizard.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.addons.jikimo_system_order.models.constant import STATE_SELECTION, GRADE
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class OrderOtherWizard(models.TransientModel):
|
||||||
|
_name = 'order.other.wizard'
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
if self._context.get('active_id'):
|
||||||
|
obj = self.env['system.work.order'].browse(self._context.get('active_id'))
|
||||||
|
if obj.initiator_id.id != self.env.user.id:
|
||||||
|
raise ValidationError(u'非本人无法操作')
|
||||||
|
return obj
|
||||||
|
|
||||||
|
order_id = fields.Many2one('system.work.order', string=u'工单ID',
|
||||||
|
default=lambda self: self.get_context().id)
|
||||||
|
# 关闭时间
|
||||||
|
close_time = fields.Datetime(string=u'关闭时间', default=fields.datetime.now())
|
||||||
|
# 状态
|
||||||
|
state = fields.Selection(STATE_SELECTION, default='completed', string=u'状态')
|
||||||
|
# 打分
|
||||||
|
grade = fields.Selection(GRADE, string=u'评分')
|
||||||
|
# 关闭人
|
||||||
|
close_user_id = fields.Many2one('res.users', string=u'关闭人', default=lambda self: self.env.user)
|
||||||
|
|
||||||
|
|
||||||
|
def sure(self):
|
||||||
|
self.order_id.close_time = self.close_time
|
||||||
|
self.order_id.grade = self.grade
|
||||||
|
if self.order_id.state == 'processed':
|
||||||
|
state_remark = u'已处理待评分 --> 已完成'
|
||||||
|
# self.order_id.message_post(u'操作人:%s,操作时间:%s,状态变更过程:%s' % (
|
||||||
|
# self.env.user.name,
|
||||||
|
# (datetime.datetime.now() + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), state_remark))
|
||||||
|
self.order_id.state = self.state
|
||||||
|
self.order_id.close_user_id = self.close_user_id
|
||||||
|
return {}
|
||||||
59
jikimo_system_order/wizard/order_technician_wizard.py
Normal file
59
jikimo_system_order/wizard/order_technician_wizard.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.addons.jikimo_system_order.models.constant import STATE_SELECTION
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class OrderTechnicianWizard(models.TransientModel):
|
||||||
|
_name = 'order.technician.wizard'
|
||||||
|
|
||||||
|
order_id = fields.Many2one('system.work.order', string=u'工单ID',
|
||||||
|
default=lambda self: self.env.context.get('active_id'))
|
||||||
|
# 解决人
|
||||||
|
solve_people_id = fields.Many2one('res.users', string=u'解决人', default=lambda self: self.env.user)
|
||||||
|
# 用户实际问题
|
||||||
|
users_problem = fields.Text(string=u'用户实际问题')
|
||||||
|
# 最终解决方案
|
||||||
|
solution = fields.Text(string=u'最终解决方案')
|
||||||
|
# 状态
|
||||||
|
state = fields.Selection(STATE_SELECTION, default='processed', string=u'状态')
|
||||||
|
|
||||||
|
def sure(self):
|
||||||
|
self.order_id.solve_people_id = self.solve_people_id
|
||||||
|
self.order_id.users_problem = self.users_problem
|
||||||
|
self.order_id.solution = self.solution
|
||||||
|
if self.order_id.state == 'pending':
|
||||||
|
state_remark = u'待处理 --> 已处理待评分'
|
||||||
|
# self.order_id.message_post(u'操作人:%s,操作时间:%s,状态变更过程:%s' % (
|
||||||
|
# self.env.user.name,
|
||||||
|
# (datetime.datetime.now() + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), state_remark))
|
||||||
|
self.order_id.state = self.state
|
||||||
|
# 获取通知人
|
||||||
|
# objs = self.env['system.order.notice'].search([])
|
||||||
|
# user_ids = objs.notice_user_ids.filtered(lambda item: item.we_employee_id not in ['', False])
|
||||||
|
# we_employee_ids = user_ids.mapped('we_employee_id')
|
||||||
|
we_employee_ids = []
|
||||||
|
if self.order_id.initiator_id.we_employee_id:
|
||||||
|
we_employee_ids.append(self.order_id.initiator_id.we_employee_id)
|
||||||
|
print(we_employee_ids)
|
||||||
|
lost_agent_id = self.env['ir.config_parameter'].sudo().get_param('lost_agent_id')
|
||||||
|
wechat = self.env['we.config'].sudo().get_wechat(agent_id=lost_agent_id)
|
||||||
|
# agent_id, user_ids, content
|
||||||
|
content = """您提交的工单-**工单标题:{0}**-<font color=\"info\">**已处理**</font>
|
||||||
|
>提交时间:{1}
|
||||||
|
>处理反馈:{4}
|
||||||
|
>处理时间:{2}
|
||||||
|
>处理人:{3}
|
||||||
|
如有问题,请联系系统管理员!
|
||||||
|
""".format(self.order_id.title,
|
||||||
|
(self.order_id.date + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M'), (datetime.datetime.now() + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M'), self.env.user.name or '', self.solution or '')
|
||||||
|
# wechat.message.send_markdown(agent_id=lost_agent_id, user_ids=we_employee_ids, content=content)
|
||||||
|
for we_employee_id in we_employee_ids:
|
||||||
|
try:
|
||||||
|
wechat.message.send_markdown(agent_id=lost_agent_id, user_ids=we_employee_id, content=content)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('工单处理发送消息异常%s' % str(e))
|
||||||
|
|
||||||
|
return {}
|
||||||
122
jikimo_system_order/wizard/order_wizard.xml
Normal file
122
jikimo_system_order/wizard/order_wizard.xml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- 技术员向导form-->
|
||||||
|
<record model="ir.ui.view" id="wizard_technician_form_view">
|
||||||
|
<field name="name">技术员向导</field>
|
||||||
|
<field name="model">order.technician.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="技术员编辑">
|
||||||
|
<group>
|
||||||
|
<field name="order_id" required="1" readonly="1"/>
|
||||||
|
<field name="solve_people_id" required="1"/>
|
||||||
|
<field name="users_problem" required="1" style="height: 50px;"/>
|
||||||
|
<field name="solution" required="1" style="height: 50px;"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="sure" string="确定" type="object" class="oe_highlight"/>
|
||||||
|
or
|
||||||
|
<button string="取消" class="oe_link" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.actions.act_window" id="launch_order_technician_wizard">
|
||||||
|
<field name="name">技术员编辑</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">order.technician.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="wizard_technician_form_view"/>
|
||||||
|
<field name="context">{'display_default_code':False}</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 其它向导form-->
|
||||||
|
<record model="ir.ui.view" id="wizard_other_form_view">
|
||||||
|
<field name="name">其它向导</field>
|
||||||
|
<field name="model">order.other.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="其它编辑">
|
||||||
|
<group>
|
||||||
|
<field name="order_id" required="1" readonly="1"/>
|
||||||
|
<field name="close_time" required="1" readonly="1"/>
|
||||||
|
<field name="grade" required="1"/>
|
||||||
|
<field name="close_user_id" required="1" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="sure" string="确定" type="object" class="oe_highlight"/>
|
||||||
|
or
|
||||||
|
<button string="取消" class="oe_link" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.actions.act_window" id="launch_order_other_wizard">
|
||||||
|
<field name="name">其它编辑</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">order.other.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="wizard_other_form_view"/>
|
||||||
|
<field name="context">{'display_default_code':False}</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!--关闭向导form-->
|
||||||
|
<record model="ir.ui.view" id="wizard_close_form_view">
|
||||||
|
<field name="name">关闭向导</field>
|
||||||
|
<field name="model">order.close.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="关闭工单">
|
||||||
|
<group>
|
||||||
|
<field name="order_id" required="1" readonly="1"/>
|
||||||
|
<field name="close_cause" required="1" style="height: 50px;"/>
|
||||||
|
<field name="close_time" required="1" readonly="1"/>
|
||||||
|
<field name="close_user_id" required="1" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="sure" string="确定" type="object" class="oe_highlight"/>
|
||||||
|
or
|
||||||
|
<button string="取消" class="oe_link" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.actions.act_window" id="launch_order_close_wizard">
|
||||||
|
<field name="name">关闭工单</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">order.close.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="wizard_close_form_view"/>
|
||||||
|
<field name="context">{'display_default_code':False}</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="system_work_order_wizard_view" model="ir.ui.view">
|
||||||
|
<field name="name">system_work_order_wizard_view</field>
|
||||||
|
<field name="model">system.work.order.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="二次确认">
|
||||||
|
<field name="explain" readonly="1"/>
|
||||||
|
<footer>
|
||||||
|
<button name="sure" string="确定" type="object" class="oe_highlight"/>
|
||||||
|
or
|
||||||
|
<button string="取消" class="oe_link" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.actions.act_window" id="system_work_order_wizard_view_act_window">
|
||||||
|
<field name="name">二次确认</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">system.work.order.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
42
jikimo_system_order/wizard/system_work_order_wizard.py
Normal file
42
jikimo_system_order/wizard/system_work_order_wizard.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# @Time : 2017/12/12 9:46
|
||||||
|
# @Author : GuoXiang
|
||||||
|
# @Site :
|
||||||
|
# @File : system_work_order_wizard.py
|
||||||
|
# @Software: PyCharm
|
||||||
|
# @Desc :
|
||||||
|
# @license : Copyright©2018 www.dasmaster.com All Rights Reserved.
|
||||||
|
# @Contact : xg1230205321@163.com
|
||||||
|
from odoo import models, api, fields
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class SystemWorkOrderWizard(models.TransientModel):
|
||||||
|
_name = "system.work.order.wizard"
|
||||||
|
_description = u"追回确认"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_explain(self):
|
||||||
|
if self._context.get('object_id'):
|
||||||
|
obj = self.env['system.work.order'].browse(self._context.get('object_id'))
|
||||||
|
if obj.initiator_id.id != self.env.user.id:
|
||||||
|
raise ValidationError(u'非本人无法操作')
|
||||||
|
if self._context.get('explain'):
|
||||||
|
return self._context["explain"]
|
||||||
|
|
||||||
|
explain = fields.Char(default=_get_explain)
|
||||||
|
|
||||||
|
|
||||||
|
def sure(self):
|
||||||
|
"""
|
||||||
|
确认
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self._context.get('object_id') and self._context.get('object_name') and self._context.get(
|
||||||
|
'explain') and self._context.get('function_name'):
|
||||||
|
work_sheet_obj = self.env[self._context["object_name"]].search([('id', '=', int(self._context["object_id"]))])
|
||||||
|
class_name = self._context.get('object_name') # 获得对象类名
|
||||||
|
method_name = self._context.get('function_name') # 获得对象的方法
|
||||||
|
obj_function = getattr(self.env[class_name], method_name)
|
||||||
|
obj_function(work_sheet_obj)
|
||||||
@@ -61,12 +61,10 @@ class MrsMaterialModel(models.Model):
|
|||||||
supplier_ids = fields.One2many('sf.supplier.sort', 'materials_model_id', string='供应商')
|
supplier_ids = fields.One2many('sf.supplier.sort', 'materials_model_id', string='供应商')
|
||||||
active = fields.Boolean('有效', default=True)
|
active = fields.Boolean('有效', default=True)
|
||||||
|
|
||||||
@api.onchange('gain_way')
|
@api.constrains("gain_way")
|
||||||
def _check_gain_way(self):
|
def _check_supplier_ids(self):
|
||||||
if not self.gain_way:
|
for item in self:
|
||||||
raise UserError("请选择获取方式")
|
if item.gain_way in ('外协', '采购') and not item.supplier_ids:
|
||||||
if self.gain_way in ['外协', '采购']:
|
|
||||||
if not self.supplier_ids:
|
|
||||||
raise UserError("请添加供应商")
|
raise UserError("请添加供应商")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,7 @@
|
|||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="materials_no" readonly="1" force_save="1"/>
|
<field name="materials_no" readonly="1" force_save="1"/>
|
||||||
<field name="gain_way" required="1"/>
|
<field name="gain_way" required="0"/>
|
||||||
<field name="tensile_strength" required="1"/>
|
<field name="tensile_strength" required="1"/>
|
||||||
<field name="hardness" required="1"/>
|
<field name="hardness" required="1"/>
|
||||||
<field name="density" readonly="1"/>
|
<field name="density" readonly="1"/>
|
||||||
@@ -270,9 +270,9 @@
|
|||||||
<notebook>
|
<notebook>
|
||||||
<page string="供应商">
|
<page string="供应商">
|
||||||
<field name='supplier_ids' class="supplier_ids_set_css">
|
<field name='supplier_ids' class="supplier_ids_set_css">
|
||||||
<tree editable='bottom'>
|
<tree editable='bottom' delete="1">
|
||||||
<field name="sequence" widget="handle" string="序号"/>
|
<field name="sequence" widget="handle" string="序号"/>
|
||||||
<field name="partner_id" string="名称"/>
|
<field name="partner_id" string="名称" options="{'no_create': True}"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
|
|||||||
@@ -161,6 +161,9 @@ class JdEclp(models.Model):
|
|||||||
url2 = config['bfm_url'] + '/api/get/jd/no'
|
url2 = config['bfm_url'] + '/api/get/jd/no'
|
||||||
response = requests.post(url2, json=json2, data=None)
|
response = requests.post(url2, json=json2, data=None)
|
||||||
# _logger.info('调用成功2', response.json()['result']['wbNo'])
|
# _logger.info('调用成功2', response.json()['result']['wbNo'])
|
||||||
|
tem_ret = response.json().get('result')
|
||||||
|
if not tem_ret:
|
||||||
|
raise ValidationError('京东物流返回异常,请联系管理员')
|
||||||
self.carrier_tracking_ref = response.json()['result'].get('wbNo')
|
self.carrier_tracking_ref = response.json()['result'].get('wbNo')
|
||||||
if not self.carrier_tracking_ref:
|
if not self.carrier_tracking_ref:
|
||||||
raise ValidationError('物流下单未成功,请联系管理员')
|
raise ValidationError('物流下单未成功,请联系管理员')
|
||||||
|
|||||||
@@ -22,6 +22,16 @@
|
|||||||
<field name="company_id" ref="base.main_company"/>
|
<field name="company_id" ref="base.main_company"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="stock_location_tool_dismantle" model="stock.location">
|
||||||
|
<field name="name">拆解</field>
|
||||||
|
<field name="location_id" ref="stock.stock_location_locations_virtual"/>
|
||||||
|
<field name="usage">internal</field>
|
||||||
|
<field name="barcode">DJCJ</field>
|
||||||
|
<field name="scrap_location">true</field>
|
||||||
|
<field name="active">true</field>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<record id="after_assembly_picking_in" model="stock.picking.type">
|
<record id="after_assembly_picking_in" model="stock.picking.type">
|
||||||
<field name="name">刀具组装入库</field>
|
<field name="name">刀具组装入库</field>
|
||||||
|
|||||||
@@ -124,12 +124,17 @@ class MrpProduction(models.Model):
|
|||||||
if (
|
if (
|
||||||
production.state == 'to_close' or production.state == 'progress') and production.schedule_state == '未排':
|
production.state == 'to_close' or production.state == 'progress') and production.schedule_state == '未排':
|
||||||
production.state = 'confirmed'
|
production.state = 'confirmed'
|
||||||
|
elif production.state == 'pending_cam' and production.schedule_state == '未排':
|
||||||
|
production.state = 'confirmed'
|
||||||
elif production.state == 'to_close' and production.schedule_state == '已排':
|
elif production.state == 'to_close' and production.schedule_state == '已排':
|
||||||
production.state = 'pending_cam'
|
production.state = 'pending_cam'
|
||||||
|
|
||||||
if production.state == 'progress':
|
if production.state == 'progress':
|
||||||
if all(wo_state not in ('progress', 'done') for wo_state in production.workorder_ids.mapped('state')):
|
if all(wo_state not in ('progress', 'done') for wo_state in production.workorder_ids.mapped('state')):
|
||||||
production.state = 'pending_cam'
|
production.state = 'pending_cam'
|
||||||
|
if production.state == 'pending_cam':
|
||||||
|
if all(wo_state in 'done' for wo_state in production.workorder_ids.mapped('state')):
|
||||||
|
production.state = 'done'
|
||||||
|
|
||||||
def action_check(self):
|
def action_check(self):
|
||||||
"""
|
"""
|
||||||
@@ -699,7 +704,7 @@ class MrpProduction(models.Model):
|
|||||||
logging.info('qty_produced:%s' % production.qty_produced)
|
logging.info('qty_produced:%s' % production.qty_produced)
|
||||||
production.write({
|
production.write({
|
||||||
'date_finished': fields.Datetime.now(),
|
'date_finished': fields.Datetime.now(),
|
||||||
'product_qty': production.product_qty if production.qty_produced < 1.0 else production.qty_produced,
|
'product_qty': production.qty_produced,
|
||||||
'priority': '0',
|
'priority': '0',
|
||||||
'is_locked': True,
|
'is_locked': True,
|
||||||
'state': 'done',
|
'state': 'done',
|
||||||
|
|||||||
@@ -5,24 +5,49 @@ from odoo.addons.resource.models.resource import Intervals
|
|||||||
|
|
||||||
|
|
||||||
class ResWorkcenter(models.Model):
|
class ResWorkcenter(models.Model):
|
||||||
_inherit = "mrp.workcenter"
|
_name = "mrp.workcenter"
|
||||||
|
_inherit = ['mrp.workcenter', 'mail.thread']
|
||||||
|
|
||||||
# 生产线显示
|
# 生产线显示
|
||||||
production_line_show = fields.Char(string='生产线名称')
|
production_line_show = fields.Char(string='生产线名称')
|
||||||
equipment_id = fields.Many2one(
|
equipment_id = fields.Many2one('maintenance.equipment', string="设备", tracking=True)
|
||||||
'maintenance.equipment', string="设备",
|
|
||||||
)
|
|
||||||
production_line_id = fields.Many2one('sf.production.line', string='生产线',
|
production_line_id = fields.Many2one('sf.production.line', string='生产线',
|
||||||
related='equipment_id.production_line_id', store=True)
|
related='equipment_id.production_line_id', store=True)
|
||||||
|
|
||||||
is_process_outsourcing = fields.Boolean('工艺外协')
|
is_process_outsourcing = fields.Boolean('工艺外协')
|
||||||
users_ids = fields.Many2many("res.users", 'users_workcenter')
|
users_ids = fields.Many2many("res.users", 'users_workcenter', tracking=True)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if 'users_ids' in vals:
|
||||||
|
old_users = self.users_ids
|
||||||
|
res = super(ResWorkcenter, self).write(vals)
|
||||||
|
new_users = self.users_ids
|
||||||
|
added_users = new_users - old_users
|
||||||
|
removed_users = old_users - new_users
|
||||||
|
if added_users or removed_users:
|
||||||
|
message = "增加 → %s ; 移除 → %s (可操作用户)" % (
|
||||||
|
# ','.join(added_users.mapped('name')), ','.join(removed_users.mapped('name')))
|
||||||
|
added_users.mapped('name'), removed_users.mapped('name'))
|
||||||
|
self.message_post(body=message)
|
||||||
|
return res
|
||||||
|
return super(ResWorkcenter, self).write(vals)
|
||||||
|
|
||||||
|
name = fields.Char('Work Center', related='resource_id.name', store=True, readonly=False, tracking=True)
|
||||||
|
time_efficiency = fields.Float('Time Efficiency', related='resource_id.time_efficiency', default=100, store=True,
|
||||||
|
readonly=False, tracking=True)
|
||||||
|
default_capacity = fields.Float(
|
||||||
|
'Capacity', default=1.0,
|
||||||
|
help="Default number of pieces (in product UoM) that can be produced in parallel (at the same time) at this work center. For example: the capacity is 5 and you need to produce 10 units, then the operation time listed on the BOM will be multiplied by two. However, note that both time before and after production will only be counted once.",
|
||||||
|
tracking=True)
|
||||||
|
|
||||||
|
oee_target = fields.Float(
|
||||||
|
string='OEE Target', help="Overall Effective Efficiency Target in percentage", default=90, tracking=True)
|
||||||
|
|
||||||
|
time_start = fields.Float('Setup Time', tracking=True)
|
||||||
|
time_stop = fields.Float('Cleanup Time', tracking=True)
|
||||||
|
costs_hour = fields.Float(string='Cost per hour', help='Hourly processing cost.', default=0.0, tracking=True)
|
||||||
|
|
||||||
equipment_status = fields.Selection(
|
equipment_status = fields.Selection(
|
||||||
[("正常", "正常"), ("故障停机", "故障停机"), ("计划维保", "计划维保"),("空闲", "空闲"),("封存(报废)", "封存(报废)")],
|
[("正常", "正常"), ("故障停机", "故障停机"), ("计划维保", "计划维保"), ("空闲", "空闲"), ("封存(报废)", "封存(报废)")],
|
||||||
string="设备状态", related='equipment_id.state')
|
string="设备状态", related='equipment_id.state')
|
||||||
|
|
||||||
# @api.depends('equipment_id')
|
# @api.depends('equipment_id')
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ class ResMrpWorkOrder(models.Model):
|
|||||||
保存名称
|
保存名称
|
||||||
"""
|
"""
|
||||||
for record in self:
|
for record in self:
|
||||||
record.save_name = record.production_id.name.replace('/', '_')
|
tem_name = record.production_id.name.replace('/', '_')
|
||||||
|
record.save_name = tem_name + '_' + record.processing_panel
|
||||||
|
|
||||||
schedule_state = fields.Selection(related='production_id.schedule_state', store=True)
|
schedule_state = fields.Selection(related='production_id.schedule_state', store=True)
|
||||||
# 工件装夹信息
|
# 工件装夹信息
|
||||||
@@ -1039,7 +1040,7 @@ class ResMrpWorkOrder(models.Model):
|
|||||||
move_raw_id.quantity_done = move_raw_id.product_uom_qty
|
move_raw_id.quantity_done = move_raw_id.product_uom_qty
|
||||||
record.process_state = '已完工'
|
record.process_state = '已完工'
|
||||||
record.production_id.process_state = '已完工'
|
record.production_id.process_state = '已完工'
|
||||||
if record.routing_type in ['解除装夹', '表面工艺']:
|
if record.routing_type in ['表面工艺']:
|
||||||
raw_move = self.env['stock.move'].sudo().search(
|
raw_move = self.env['stock.move'].sudo().search(
|
||||||
[('origin', '=', record.production_id.name),
|
[('origin', '=', record.production_id.name),
|
||||||
('procure_method', 'in', ['make_to_order', 'make_to_stock']),
|
('procure_method', 'in', ['make_to_order', 'make_to_stock']),
|
||||||
@@ -1115,6 +1116,16 @@ class ResMrpWorkOrder(models.Model):
|
|||||||
# logging.info('button_send_program_again error:%s' % e)
|
# logging.info('button_send_program_again error:%s' % e)
|
||||||
# raise UserError("重新下发nc程序失败,请联系管理员")
|
# raise UserError("重新下发nc程序失败,请联系管理员")
|
||||||
|
|
||||||
|
def print_method(self):
|
||||||
|
"""
|
||||||
|
解除装夹处调用关联制造订单的关联序列号的打印方法
|
||||||
|
"""
|
||||||
|
if self.production_id:
|
||||||
|
if self.production_id.lot_producing_id:
|
||||||
|
self.production_id.lot_producing_id.print_single_method()
|
||||||
|
else:
|
||||||
|
raise UserError("无关联制造订单或关联序列号,无法打印。请检查!")
|
||||||
|
|
||||||
|
|
||||||
class CNCprocessing(models.Model):
|
class CNCprocessing(models.Model):
|
||||||
_name = 'sf.cnc.processing'
|
_name = 'sf.cnc.processing'
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ class StockRule(models.Model):
|
|||||||
productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(
|
productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(
|
||||||
productions_values)
|
productions_values)
|
||||||
|
|
||||||
self.env['stock.move'].sudo().create(productions._get_moves_raw_values())
|
# self.env['stock.move'].sudo().create(productions._get_moves_raw_values())
|
||||||
|
self.env['stock.move'].sudo().create(productions._get_moves_finished_values())
|
||||||
|
|
||||||
'''
|
'''
|
||||||
创建工单
|
创建工单
|
||||||
@@ -404,6 +405,7 @@ class ProductionLot(models.Model):
|
|||||||
|
|
||||||
def print_single_method(self):
|
def print_single_method(self):
|
||||||
|
|
||||||
|
print('self.name========== %s' % self.name)
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
qr_code_data = self.qr_code_image
|
qr_code_data = self.qr_code_image
|
||||||
if not qr_code_data:
|
if not qr_code_data:
|
||||||
|
|||||||
@@ -24,6 +24,20 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="custom_model_form_view_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">custom.model.form.view.inherit</field>
|
||||||
|
<field name="model">mrp.workcenter</field>
|
||||||
|
<field name="inherit_id" ref="mrp.mrp_workcenter_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr='//form//sheet' position="after">
|
||||||
|
<div class="oe_chatter">
|
||||||
|
<field name="message_follower_ids"/>
|
||||||
|
<field name="message_ids"/>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="mrp_workcenter_view_kanban_inherit_workorder" model="ir.ui.view">
|
<record id="mrp_workcenter_view_kanban_inherit_workorder" model="ir.ui.view">
|
||||||
<field name="name">mrp.workcenter.view.kanban.inherit.mrp.workorder</field>
|
<field name="name">mrp.workcenter.view.kanban.inherit.mrp.workorder</field>
|
||||||
<field name="model">mrp.workcenter</field>
|
<field name="model">mrp.workcenter</field>
|
||||||
|
|||||||
@@ -160,9 +160,19 @@
|
|||||||
<!-- attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done', 'cancel')), ('working_state', '!=', 'blocked'),('state','=','done')]}"/> -->
|
<!-- attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done', 'cancel')), ('working_state', '!=', 'blocked'),('state','=','done')]}"/> -->
|
||||||
<button name="button_workpiece_delivery" type="object" string="工件配送" class="btn-primary"
|
<button name="button_workpiece_delivery" type="object" string="工件配送" class="btn-primary"
|
||||||
attrs="{'invisible': ['|','|',('routing_type','!=','装夹预调'),('is_delivery','=',True),('state','!=','done')]}"/>
|
attrs="{'invisible': ['|','|',('routing_type','!=','装夹预调'),('is_delivery','=',True),('state','!=','done')]}"/>
|
||||||
|
<<<<<<< HEAD
|
||||||
<button name="button_rework_pre" type="object" string="返工"
|
<button name="button_rework_pre" type="object" string="返工"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
attrs="{'invisible': ['|','|',('routing_type','!=','装夹预调'),('state','!=','progress'),('is_rework','=',True)]}"/>
|
attrs="{'invisible': ['|','|',('routing_type','!=','装夹预调'),('state','!=','progress'),('is_rework','=',True)]}"/>
|
||||||
|
=======
|
||||||
|
<!-- <button name="button_send_program_again" type="object" string="重新下发NC程序" class="btn-primary"-->
|
||||||
|
<!-- confirm="是否确认重新下发NC程序?"-->
|
||||||
|
<!-- groups="sf_base.group_sf_order_user,sf_base.group_sf_equipment_user"-->
|
||||||
|
<!-- attrs="{'invisible': ['|', '|', '|',('routing_type','!=','装夹预调'),('state','in',['done', 'cancel',-->
|
||||||
|
<!-- 'progress']),('cnc_worksheet','=',False),('is_send_program_again','=',True)]}"/>-->
|
||||||
|
<button name="print_method" type="object" string="打印二维码" class="btn-primary"
|
||||||
|
attrs="{'invisible': ['|',('routing_type','!=','解除装夹'),('state','!=','done')]}"/>
|
||||||
|
>>>>>>> 6a2ff72dce60612aeb6c6f2e1851081840f46322
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//page[1]" position="before">
|
<xpath expr="//page[1]" position="before">
|
||||||
<page string="开料要求" attrs='{"invisible": [("routing_type","!=","切割")]}'>
|
<page string="开料要求" attrs='{"invisible": [("routing_type","!=","切割")]}'>
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class ResaleOrderLine(models.Model):
|
|||||||
# # without modifying the related product_id when updated.
|
# # without modifying the related product_id when updated.
|
||||||
# domain=[('sale_ok', '=', True), ('categ_type', '=', '成品')])
|
# domain=[('sale_ok', '=', True), ('categ_type', '=', '成品')])
|
||||||
check_status = fields.Selection(related='order_id.check_status')
|
check_status = fields.Selection(related='order_id.check_status')
|
||||||
|
remark = fields.Char('备注')
|
||||||
|
|
||||||
@api.depends('product_template_id')
|
@api.depends('product_template_id')
|
||||||
def _compute_model_glb_file(self):
|
def _compute_model_glb_file(self):
|
||||||
@@ -256,33 +257,33 @@ class ResPartnerToSale(models.Model):
|
|||||||
# if obj:
|
# if obj:
|
||||||
# raise UserError('该邮箱已存在,请重新输入')
|
# raise UserError('该邮箱已存在,请重新输入')
|
||||||
|
|
||||||
@api.model
|
# @api.model
|
||||||
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
|
# def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
|
||||||
if self._context.get('is_customer'):
|
# if self._context.get('is_customer'):
|
||||||
if self.env.user.has_group('sf_base.group_sale_director'):
|
# if self.env.user.has_group('sf_base.group_sale_director'):
|
||||||
domain = [('customer_rank', '>', 0)]
|
# domain = [('customer_rank', '>', 0)]
|
||||||
elif self.env.user.has_group('sf_base.group_sale_salemanager'):
|
# elif self.env.user.has_group('sf_base.group_sale_salemanager'):
|
||||||
customer = self.env['res.partner'].search(
|
# customer = self.env['res.partner'].search(
|
||||||
[('customer_rank', '>', 0), ('user_id', '=', self.env.user.id)])
|
# [('customer_rank', '>', 0), ('user_id', '=', self.env.user.id)])
|
||||||
if customer:
|
# if customer:
|
||||||
ids = [t.id for t in customer]
|
# ids = [t.id for t in customer]
|
||||||
domain = [('id', 'in', ids)]
|
# domain = [('id', 'in', ids)]
|
||||||
else:
|
# else:
|
||||||
domain = [('id', '=', False)]
|
# domain = [('id', '=', False)]
|
||||||
return self._search(domain, limit=limit, access_rights_uid=name_get_uid)
|
# return self._search(domain, limit=limit, access_rights_uid=name_get_uid)
|
||||||
elif self._context.get('is_supplier') or self.env.user.has_group('sf_base.group_purchase_director'):
|
# elif self._context.get('is_supplier') or self.env.user.has_group('sf_base.group_purchase_director'):
|
||||||
if self.env.user.has_group('sf_base.group_purchase_director'):
|
# if self.env.user.has_group('sf_base.group_purchase_director'):
|
||||||
domain = [('supplier_rank', '>', 0)]
|
# domain = [('supplier_rank', '>', 0)]
|
||||||
elif self.env.user.has_group('sf_base.group_purchase'):
|
# elif self.env.user.has_group('sf_base.group_purchase'):
|
||||||
supplier = self.env['res.partner'].search(
|
# supplier = self.env['res.partner'].search(
|
||||||
[('supplier_rank', '>', 0), ('purchase_user_id', '=', self.env.user.id)])
|
# [('supplier_rank', '>', 0), ('purchase_user_id', '=', self.env.user.id)])
|
||||||
if supplier:
|
# if supplier:
|
||||||
ids = [t.id for t in supplier]
|
# ids = [t.id for t in supplier]
|
||||||
domain = [('id', 'in', ids)]
|
# domain = [('id', 'in', ids)]
|
||||||
else:
|
# else:
|
||||||
domain = [('id', '=', False)]
|
# domain = [('id', '=', False)]
|
||||||
return self._search(domain, limit=limit, access_rights_uid=name_get_uid)
|
# return self._search(domain, limit=limit, access_rights_uid=name_get_uid)
|
||||||
return super()._name_search(name, args, operator, limit, name_get_uid)
|
# return super()._name_search(name, args, operator, limit, name_get_uid)
|
||||||
|
|
||||||
@api.onchange('user_id')
|
@api.onchange('user_id')
|
||||||
def _get_salesman(self):
|
def _get_salesman(self):
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
<field name="model_glb_file" widget="Viewer3D" optional="show"
|
<field name="model_glb_file" widget="Viewer3D" optional="show"
|
||||||
string="模型文件" attrs="{'readonly': [('state', 'in', ['draft'])]}"/>
|
string="模型文件" attrs="{'readonly': [('state', 'in', ['draft'])]}"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='order_line']/tree/field[@name='price_subtotal']" position="after">
|
||||||
|
<field name="remark"/>
|
||||||
|
</xpath>
|
||||||
<xpath expr="//field[@name='order_line']/tree/field[@name='product_template_id']" position="attributes">
|
<xpath expr="//field[@name='order_line']/tree/field[@name='product_template_id']" position="attributes">
|
||||||
<attribute name="options">{'no_create': True}</attribute>
|
<attribute name="options">{'no_create': True}</attribute>
|
||||||
<attribute name="context">{'is_sale_order_line': True }</attribute>
|
<attribute name="context">{'is_sale_order_line': True }</attribute>
|
||||||
|
|||||||
@@ -669,6 +669,20 @@ class FunctionalToolAssembly(models.Model):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
picking_num = fields.Integer('调拨单数量', compute='compute_picking_num', store=True)
|
||||||
|
|
||||||
|
@api.depends('assemble_status')
|
||||||
|
def compute_picking_num(self):
|
||||||
|
for item in self:
|
||||||
|
picking_ids = self.env['stock.picking'].sudo().search([('origin', '=', item.assembly_order_code)])
|
||||||
|
item.picking_num = len(picking_ids)
|
||||||
|
|
||||||
|
def open_tool_stock_picking(self):
|
||||||
|
action = self.env.ref('stock.action_picking_tree_all')
|
||||||
|
result = action.read()[0]
|
||||||
|
result['domain'] = [('origin', '=', self.assembly_order_code)]
|
||||||
|
return result
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
obj = super(FunctionalToolAssembly, self).create(vals)
|
obj = super(FunctionalToolAssembly, self).create(vals)
|
||||||
@@ -758,7 +772,7 @@ class FunctionalToolDismantle(models.Model):
|
|||||||
num = "%03d" % m
|
num = "%03d" % m
|
||||||
return 'GNDJ-CJD-%s-%s' % (datetime, num)
|
return 'GNDJ-CJD-%s-%s' % (datetime, num)
|
||||||
|
|
||||||
functional_tool_id = fields.Many2one('sf.functional.cutting.tool.entity', '功能刀具', required=True,
|
functional_tool_id = fields.Many2one('sf.functional.cutting.tool.entity', '功能刀具', required=True, tracking=True,
|
||||||
domain=[('functional_tool_status', '!=', '已拆除'),
|
domain=[('functional_tool_status', '!=', '已拆除'),
|
||||||
('current_location', '=', '刀具房')])
|
('current_location', '=', '刀具房')])
|
||||||
tool_type_id = fields.Many2one('sf.functional.cutting.tool.model', string='功能刀具类型', store=True,
|
tool_type_id = fields.Many2one('sf.functional.cutting.tool.model', string='功能刀具类型', store=True,
|
||||||
@@ -776,8 +790,18 @@ class FunctionalToolDismantle(models.Model):
|
|||||||
dismantle_person = fields.Char('拆解人', readonly=True)
|
dismantle_person = fields.Char('拆解人', readonly=True)
|
||||||
image = fields.Binary('图片', readonly=True)
|
image = fields.Binary('图片', readonly=True)
|
||||||
|
|
||||||
scrap_id = fields.Char('报废单号', readonly=True)
|
scrap_ids = fields.One2many('stock.scrap', 'functional_tool_dismantle_id', string='报废单号', readonly=True)
|
||||||
grinding_id = fields.Char('磨削单号', readonly=True)
|
grinding_id = fields.Char('磨削单号', readonly=True)
|
||||||
|
picking_id = fields.Many2one('stock.picking', string='刀具物料调拨单')
|
||||||
|
picking_num = fields.Integer('调拨单数量', default=0, compute='compute_picking_num', store=True)
|
||||||
|
|
||||||
|
@api.depends('picking_id')
|
||||||
|
def compute_picking_num(self):
|
||||||
|
for item in self:
|
||||||
|
if item.picking_id:
|
||||||
|
item.picking_num = 1
|
||||||
|
else:
|
||||||
|
item.picking_num = 0
|
||||||
|
|
||||||
state = fields.Selection([('待拆解', '待拆解'), ('已拆解', '已拆解')], default='待拆解', tracking=True)
|
state = fields.Selection([('待拆解', '待拆解'), ('已拆解', '已拆解')], default='待拆解', tracking=True)
|
||||||
active = fields.Boolean('有效', default=True)
|
active = fields.Boolean('有效', default=True)
|
||||||
@@ -791,7 +815,14 @@ class FunctionalToolDismantle(models.Model):
|
|||||||
handle_rfid = fields.Char(string='刀柄Rfid', compute='_compute_functional_tool_num', store=True)
|
handle_rfid = fields.Char(string='刀柄Rfid', compute='_compute_functional_tool_num', store=True)
|
||||||
handle_lot_id = fields.Many2one('stock.lot', string='刀柄序列号', compute='_compute_functional_tool_num',
|
handle_lot_id = fields.Many2one('stock.lot', string='刀柄序列号', compute='_compute_functional_tool_num',
|
||||||
store=True)
|
store=True)
|
||||||
scrap_boolean = fields.Boolean(string='刀柄是否报废', default=False, tracking=True)
|
scrap_boolean = fields.Boolean(string='刀柄是否报废', default=False, tracking=True, compute='compute_scrap_boolean',
|
||||||
|
store=True)
|
||||||
|
|
||||||
|
@api.depends('dismantle_cause')
|
||||||
|
def compute_scrap_boolean(self):
|
||||||
|
for item in self:
|
||||||
|
if item.dismantle_cause not in ['寿命到期报废', '崩刀报废']:
|
||||||
|
item.scrap_boolean = False
|
||||||
|
|
||||||
# 整体式
|
# 整体式
|
||||||
integral_product_id = fields.Many2one('product.product', string='整体式刀具',
|
integral_product_id = fields.Many2one('product.product', string='整体式刀具',
|
||||||
@@ -933,75 +964,50 @@ class FunctionalToolDismantle(models.Model):
|
|||||||
self.rfid, self.functional_tool_id.current_location))
|
self.rfid, self.functional_tool_id.current_location))
|
||||||
# 目标重复校验
|
# 目标重复校验
|
||||||
self.location_duplicate_check()
|
self.location_duplicate_check()
|
||||||
location = self.env['stock.location'].search([('name', '=', '刀具组装位置')])
|
datas = {'scrap': [], 'picking': []}
|
||||||
location_dest = self.env['stock.location'].search([('name', '=', '刀具房')])
|
|
||||||
# =================刀柄是否[报废]拆解=======
|
# =================刀柄是否[报废]拆解=======
|
||||||
location_dest_scrap_ids = self.env['stock.location'].search([('name', 'in', ('Scrap', '报废'))])
|
|
||||||
if self.handle_rfid:
|
if self.handle_rfid:
|
||||||
lot = self.env['stock.lot'].sudo().search([('rfid', '=', self.handle_rfid)])
|
lot = self.env['stock.lot'].sudo().search([('rfid', '=', self.handle_rfid)])
|
||||||
if not lot:
|
if not lot:
|
||||||
raise ValidationError('Rfid为【%s】的功能刀具序列号不存在!' % self.handle_rfid)
|
raise ValidationError('Rfid为【%s】的刀柄序列号不存在!' % self.handle_rfid)
|
||||||
functional_tool_assembly = self.functional_tool_id.functional_tool_name_id
|
|
||||||
if self.scrap_boolean:
|
if self.scrap_boolean:
|
||||||
# 刀柄报废 入库到Scrap
|
# 刀柄报废 入库到Scrap
|
||||||
lot.create_stock_quant(location, location_dest_scrap_ids[-1], False, code, False, False)
|
datas['scrap'].append({'lot_id': lot})
|
||||||
lot.tool_material_status = '报废'
|
lot.tool_material_status = '报废'
|
||||||
else:
|
else:
|
||||||
# 刀柄不报废 入库到刀具房
|
# 刀柄不报废 入库到刀具房
|
||||||
lot.create_stock_quant(location, location_dest, False, code, False, False)
|
datas['picking'].append({'lot_id': lot, 'destination': self.env['sf.shelf.location']})
|
||||||
lot.tool_material_status = '可用'
|
lot.tool_material_status = '可用'
|
||||||
|
|
||||||
# ==============功能刀具[报废]拆解================
|
# ==============功能刀具[报废]拆解================
|
||||||
if self.dismantle_cause in ['寿命到期报废', '崩刀报废']:
|
if self.dismantle_cause in ['寿命到期报废', '崩刀报废']:
|
||||||
# 除刀柄外物料报废 入库到Scrap
|
# 除刀柄外物料报废 入库到Scrap
|
||||||
if self.integral_product_id:
|
if self.integral_product_id:
|
||||||
self.integral_product_id.dismantle_stock_moves(False, self.integral_lot_id, location,
|
datas['scrap'].append({'lot_id': self.integral_lot_id})
|
||||||
location_dest_scrap_ids[-1], code)
|
|
||||||
elif self.blade_product_id:
|
elif self.blade_product_id:
|
||||||
self.blade_product_id.dismantle_stock_moves(False, self.blade_lot_id, location,
|
datas['scrap'].append({'lot_id': self.blade_lot_id})
|
||||||
location_dest_scrap_ids[-1], code)
|
|
||||||
if self.bar_product_id:
|
if self.bar_product_id:
|
||||||
self.bar_product_id.dismantle_stock_moves(False, self.bar_lot_id, location,
|
datas['scrap'].append({'lot_id': self.bar_lot_id})
|
||||||
location_dest_scrap_ids[-1], code)
|
|
||||||
elif self.pad_product_id:
|
elif self.pad_product_id:
|
||||||
self.pad_product_id.dismantle_stock_moves(False, self.pad_lot_id, location,
|
datas['scrap'].append({'lot_id': self.pad_lot_id})
|
||||||
location_dest_scrap_ids[-1], code)
|
|
||||||
if self.chuck_product_id:
|
if self.chuck_product_id:
|
||||||
self.chuck_product_id.dismantle_stock_moves(False, self.chuck_lot_id, location,
|
datas['scrap'].append({'lot_id': self.chuck_lot_id})
|
||||||
location_dest_scrap_ids[-1], code)
|
|
||||||
# ===========功能刀具[磨削]拆解==============
|
|
||||||
# elif self.dismantle_cause in ['刀具需磨削']:
|
|
||||||
# location_dest = self.env['stock.location'].search([('name', '=', '磨削房')])
|
|
||||||
# # 除刀柄外物料拆解 入库到具体库位
|
|
||||||
# if self.integral_product_id:
|
|
||||||
# self.integral_product_id.dismantle_stock_moves(False, location, location_dest)
|
|
||||||
# elif self.blade_product_id:
|
|
||||||
# self.blade_product_id.dismantle_stock_moves(False, location, location_dest)
|
|
||||||
# if self.bar_product_id:
|
|
||||||
# self.bar_product_id.dismantle_stock_moves(False, location, location_dest)
|
|
||||||
# elif self.pad_product_id:
|
|
||||||
# self.pad_product_id.dismantle_stock_moves(False, location, location_dest)
|
|
||||||
# if self.chuck_product_id:
|
|
||||||
# self.chuck_product_id.dismantle_stock_moves(False, location, location_dest)
|
|
||||||
# ==============功能刀具[更换,磨削]拆解==============
|
# ==============功能刀具[更换,磨削]拆解==============
|
||||||
elif self.dismantle_cause in ['更换为其他刀具', '刀具需磨削']:
|
elif self.dismantle_cause in ['更换为其他刀具', '刀具需磨削']:
|
||||||
# 除刀柄外物料拆解 入库到具体货位
|
# 除刀柄外物料拆解 入库到具体货位
|
||||||
if self.integral_freight_id:
|
if self.integral_freight_id:
|
||||||
self.integral_product_id.dismantle_stock_moves(self.integral_freight_id, self.integral_lot_id, location,
|
datas['picking'].append({'lot_id': self.integral_lot_id, 'destination': self.integral_freight_id})
|
||||||
location_dest, code)
|
|
||||||
elif self.blade_freight_id:
|
elif self.blade_freight_id:
|
||||||
self.blade_product_id.dismantle_stock_moves(self.blade_freight_id, self.blade_lot_id, location,
|
datas['picking'].append({'lot_id': self.blade_lot_id, 'destination': self.blade_freight_id})
|
||||||
location_dest, code)
|
|
||||||
if self.bar_freight_id:
|
if self.bar_freight_id:
|
||||||
self.bar_product_id.dismantle_stock_moves(self.bar_freight_id, self.bar_lot_id, location,
|
datas['picking'].append({'lot_id': self.bar_lot_id, 'destination': self.bar_freight_id})
|
||||||
location_dest, code)
|
|
||||||
elif self.pad_freight_id:
|
elif self.pad_freight_id:
|
||||||
self.pad_product_id.dismantle_stock_moves(self.pad_freight_id, self.pad_lot_id, location,
|
datas['picking'].append({'lot_id': self.pad_lot_id, 'destination': self.pad_freight_id})
|
||||||
location_dest, code)
|
|
||||||
if self.chuck_freight_id:
|
if self.chuck_freight_id:
|
||||||
self.chuck_product_id.dismantle_stock_moves(self.chuck_freight_id, self.chuck_lot_id, location,
|
datas['picking'].append({'lot_id': self.chuck_lot_id, 'destination': self.chuck_freight_id})
|
||||||
location_dest, code)
|
self.create_tool_picking_scrap(datas)
|
||||||
# ===============删除功能刀具的Rfid字段的值, 赋值给Rfid(已拆解)字段=====
|
# ===============创建功能刀具拆解移动记录=====
|
||||||
|
self.env['stock.move'].create_functional_tool_stock_move(self)
|
||||||
|
# 修改功能刀具数据
|
||||||
self.functional_tool_id.write({
|
self.functional_tool_id.write({
|
||||||
'rfid_dismantle': self.functional_tool_id.rfid,
|
'rfid_dismantle': self.functional_tool_id.rfid,
|
||||||
'rfid': '',
|
'rfid': '',
|
||||||
@@ -1009,37 +1015,173 @@ class FunctionalToolDismantle(models.Model):
|
|||||||
})
|
})
|
||||||
# 修改拆解单的值
|
# 修改拆解单的值
|
||||||
self.write({
|
self.write({
|
||||||
'rfid_dismantle': self.rfid,
|
|
||||||
'dismantle_data': fields.Datetime.now(),
|
'dismantle_data': fields.Datetime.now(),
|
||||||
'dismantle_person': self.env.user.name,
|
'dismantle_person': self.env.user.name,
|
||||||
'rfid': '',
|
'rfid': '%s(已拆解)' % self.rfid,
|
||||||
'state': '已拆解'
|
'state': '已拆解'
|
||||||
})
|
})
|
||||||
logging.info('【%s】刀具拆解成功!' % self.name)
|
logging.info('【%s】刀具拆解成功!' % self.name)
|
||||||
|
|
||||||
|
def create_tool_picking_scrap(self, datas):
|
||||||
|
scrap_data = datas['scrap']
|
||||||
|
picking_data = datas['picking']
|
||||||
|
if scrap_data:
|
||||||
|
for data in scrap_data:
|
||||||
|
if data:
|
||||||
|
self.env['stock.scrap'].create_tool_dismantle_stock_scrap(data['lot_id'], self)
|
||||||
|
if picking_data:
|
||||||
|
picking_id = self.env['stock.picking'].create_tool_dismantle_picking(self)
|
||||||
|
self.picking_id = picking_id.id
|
||||||
|
self.env['stock.move'].create_tool_stock_move({'data': picking_data, 'picking_id': picking_id})
|
||||||
|
# 将刀具物料出库库单的状态更改为就绪
|
||||||
|
picking_id.action_confirm()
|
||||||
|
# 修改刀具物料出库移动历史记录
|
||||||
|
self.env['stock.move'].write_tool_stock_move_line({'data': picking_data, 'picking_id': picking_id})
|
||||||
|
# 设置数量,并验证完成
|
||||||
|
picking_id.action_set_quantities_to_reservation()
|
||||||
|
picking_id.button_validate()
|
||||||
|
|
||||||
class ProductProduct(models.Model):
|
def action_open_reference1(self):
|
||||||
_inherit = 'product.product'
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'res_model': self._name,
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'views': [[False, "form"]],
|
||||||
|
'res_id': self.id,
|
||||||
|
}
|
||||||
|
|
||||||
def dismantle_stock_moves(self, shelf_location_id, lot_id, location_id, location_dest_id, code):
|
def open_function_tool_stock_move_line(self):
|
||||||
# 创建功能刀具拆解单产品库存移动记录
|
action = self.env.ref('sf_tool_management.sf_inbound_and_outbound_records_of_functional_tools_view_act')
|
||||||
stock_move_id = self.env['stock.move'].sudo().create({
|
result = action.read()[0]
|
||||||
'name': code,
|
result['domain'] = [('functional_tool_dismantle_id', '=', self.id), ('qty_done', '>', 0)]
|
||||||
'product_id': self.id,
|
return result
|
||||||
|
|
||||||
|
def open_tool_stock_picking(self):
|
||||||
|
action = self.env.ref('stock.action_picking_tree_all')
|
||||||
|
result = action.read()[0]
|
||||||
|
result['domain'] = [('origin', '=', self.code)]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class StockPicking(models.Model):
|
||||||
|
_inherit = 'stock.picking'
|
||||||
|
|
||||||
|
def create_tool_dismantle_picking(self, obj):
|
||||||
|
"""
|
||||||
|
创建刀具物料入库单
|
||||||
|
"""
|
||||||
|
# 获取名称为内部调拨的作业类型
|
||||||
|
picking_type_id = self.env['stock.picking.type'].sudo().search([('name', '=', '内部调拨')])
|
||||||
|
location_id = self.env['stock.location'].search([('name', '=', '刀具组装位置')])
|
||||||
|
location_dest_id = self.env['stock.location'].search([('name', '=', '刀具房')])
|
||||||
|
if not location_id:
|
||||||
|
raise ValidationError('缺少名称为【刀具组装位置】的仓库管理地点')
|
||||||
|
if not location_dest_id:
|
||||||
|
raise ValidationError('缺少名称为【刀具房】的仓库管理地点')
|
||||||
|
# 创建刀具物料出库单
|
||||||
|
picking_id = self.env['stock.picking'].create({
|
||||||
|
'name': self._get_name_stock1(picking_type_id),
|
||||||
|
'picking_type_id': picking_type_id.id,
|
||||||
'location_id': location_id.id,
|
'location_id': location_id.id,
|
||||||
'location_dest_id': location_dest_id.id,
|
'location_dest_id': location_dest_id.id,
|
||||||
|
'origin': obj.code
|
||||||
|
})
|
||||||
|
|
||||||
|
return picking_id
|
||||||
|
|
||||||
|
|
||||||
|
class StockMove(models.Model):
|
||||||
|
_inherit = 'stock.move'
|
||||||
|
|
||||||
|
def create_tool_stock_move(self, datas):
|
||||||
|
picking_id = datas['picking_id']
|
||||||
|
data = datas['data']
|
||||||
|
stock_move_ids = []
|
||||||
|
for res in data:
|
||||||
|
if res:
|
||||||
|
# 创建库存移动记录
|
||||||
|
stock_move_id = self.env['stock.move'].sudo().create({
|
||||||
|
'name': picking_id.name,
|
||||||
|
'picking_id': picking_id.id,
|
||||||
|
'product_id': res['lot_id'].product_id.id,
|
||||||
|
'location_id': picking_id.location_id.id,
|
||||||
|
'location_dest_id': picking_id.location_dest_id.id,
|
||||||
|
'product_uom_qty': 1.00,
|
||||||
|
'reserved_availability': 1.00
|
||||||
|
})
|
||||||
|
stock_move_ids.append(stock_move_id)
|
||||||
|
return stock_move_ids
|
||||||
|
|
||||||
|
def write_tool_stock_move_line(self, datas):
|
||||||
|
picking_id = datas['picking_id']
|
||||||
|
data = datas['data']
|
||||||
|
move_line_ids = picking_id.move_line_ids
|
||||||
|
for move_line_id in move_line_ids:
|
||||||
|
for res in data:
|
||||||
|
if move_line_id.lot_id.product_id == res['lot_id'].product_id:
|
||||||
|
move_line_id.write({
|
||||||
|
'destination_location_id': res.get('destination').id,
|
||||||
|
'lot_id': res.get('lot_id').id
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_functional_tool_stock_move(self, dismantle_id):
|
||||||
|
"""
|
||||||
|
对功能刀具拆解过程的功能刀具进行库存移动,以及创建移动历史
|
||||||
|
"""
|
||||||
|
location_dismantle_id = self.env['stock.location'].search([('name', '=', '拆解')])
|
||||||
|
if not location_dismantle_id:
|
||||||
|
raise ValidationError('缺少名称为【拆解】的仓库管理地点')
|
||||||
|
tool_id = dismantle_id.functional_tool_id
|
||||||
|
# 创建库存移动记录
|
||||||
|
stock_move_id = self.env['stock.move'].sudo().create({
|
||||||
|
'name': dismantle_id.code,
|
||||||
|
'product_id': tool_id.barcode_id.product_id.id,
|
||||||
|
'location_id': tool_id.current_location_id.id,
|
||||||
|
'location_dest_id': location_dismantle_id.id,
|
||||||
'product_uom_qty': 1.00,
|
'product_uom_qty': 1.00,
|
||||||
'state': 'done'
|
'state': 'done'
|
||||||
})
|
})
|
||||||
|
|
||||||
# 创建移动历史记录
|
# 创建移动历史记录
|
||||||
stock_move_line_id = self.env['stock.move.line'].sudo().create({
|
stock_move_line_id = self.env['stock.move.line'].sudo().create({
|
||||||
'product_id': self.id,
|
'product_id': tool_id.barcode_id.product_id.id,
|
||||||
'lot_id': lot_id.id,
|
'functional_tool_dismantle_id': dismantle_id.id,
|
||||||
|
'lot_id': tool_id.barcode_id.id,
|
||||||
'move_id': stock_move_id.id,
|
'move_id': stock_move_id.id,
|
||||||
'destination_location_id': shelf_location_id.id if shelf_location_id else False,
|
|
||||||
'install_tool_time': fields.Datetime.now(),
|
|
||||||
'qty_done': 1.0,
|
'qty_done': 1.0,
|
||||||
'state': 'done',
|
'state': 'done',
|
||||||
|
'functional_tool_type_id': tool_id.sf_cutting_tool_type_id.id,
|
||||||
|
'diameter': tool_id.functional_tool_diameter,
|
||||||
|
'knife_tip_r_angle': tool_id.knife_tip_r_angle,
|
||||||
|
'code': tool_id.code,
|
||||||
|
'rfid': tool_id.rfid,
|
||||||
|
'functional_tool_name': tool_id.name,
|
||||||
|
'tool_groups_id': tool_id.tool_groups_id.id
|
||||||
})
|
})
|
||||||
|
|
||||||
return stock_move_id, stock_move_line_id
|
return stock_move_id, stock_move_line_id
|
||||||
|
|
||||||
|
|
||||||
|
class CustomStockScrap(models.Model):
|
||||||
|
_inherit = 'stock.scrap'
|
||||||
|
|
||||||
|
functional_tool_dismantle_id = fields.Many2one('sf.functional.tool.dismantle', string="功能刀具拆解单")
|
||||||
|
|
||||||
|
def create_tool_dismantle_stock_scrap(self, lot, dismantle_id):
|
||||||
|
location_id = self.env['stock.location'].search([('name', '=', '刀具组装位置')])
|
||||||
|
scrap_location_id = self.env['stock.location'].search([('name', 'in', ('Scrap', '报废'))])
|
||||||
|
if not location_id:
|
||||||
|
raise ValidationError('缺少名称为【刀具组装位置】的仓库管理地点')
|
||||||
|
if not scrap_location_id:
|
||||||
|
raise ValidationError('缺少名称为【Scrap】或【Scrap】的仓库管理地点')
|
||||||
|
stock_scrap_id = self.create({
|
||||||
|
'product_id': lot.product_id.id,
|
||||||
|
'lot_id': lot.id,
|
||||||
|
'location_id': location_id.id,
|
||||||
|
'scrap_location_id': scrap_location_id.id,
|
||||||
|
'functional_tool_dismantle_id': dismantle_id.id,
|
||||||
|
'origin': dismantle_id.code
|
||||||
|
})
|
||||||
|
# 完成报废单
|
||||||
|
stock_scrap_id.action_validate()
|
||||||
|
return stock_scrap_id
|
||||||
|
|||||||
@@ -252,9 +252,7 @@ class FunctionalCuttingToolEntity(models.Model):
|
|||||||
def open_safety_stock(self):
|
def open_safety_stock(self):
|
||||||
action = self.env.ref('sf_tool_management.sf_real_time_distribution_of_functional_tools_view_act')
|
action = self.env.ref('sf_tool_management.sf_real_time_distribution_of_functional_tools_view_act')
|
||||||
result = action.read()[0]
|
result = action.read()[0]
|
||||||
result['domain'] = [('name', '=', self.name), ('diameter', '=', self.functional_tool_diameter),
|
result['domain'] = [('id', '=', self.safe_inventory_id.id)]
|
||||||
('knife_tip_r_angle', '=', self.knife_tip_r_angle),
|
|
||||||
('coarse_middle_thin', '=', self.coarse_middle_thin)]
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def tool_inventory_displacement_out(self):
|
def tool_inventory_displacement_out(self):
|
||||||
@@ -372,6 +370,7 @@ class StockMoveLine(models.Model):
|
|||||||
_order = 'date desc'
|
_order = 'date desc'
|
||||||
|
|
||||||
functional_tool_name_id = fields.Many2one('sf.functional.tool.assembly', string='功能刀具组装单')
|
functional_tool_name_id = fields.Many2one('sf.functional.tool.assembly', string='功能刀具组装单')
|
||||||
|
functional_tool_dismantle_id = fields.Many2one('sf.functional.tool.dismantle', string='功能刀具拆解单')
|
||||||
functional_tool_type_id = fields.Many2one('sf.functional.cutting.tool.model', string='功能刀具类型', store=True,
|
functional_tool_type_id = fields.Many2one('sf.functional.cutting.tool.model', string='功能刀具类型', store=True,
|
||||||
group_expand='_read_group_functional_tool_type_id')
|
group_expand='_read_group_functional_tool_type_id')
|
||||||
functional_tool_name = fields.Char('刀具名称')
|
functional_tool_name = fields.Char('刀具名称')
|
||||||
@@ -392,6 +391,9 @@ class StockMoveLine(models.Model):
|
|||||||
if self.functional_tool_name_id:
|
if self.functional_tool_name_id:
|
||||||
action = self.functional_tool_name_id.action_open_reference1()
|
action = self.functional_tool_name_id.action_open_reference1()
|
||||||
return action
|
return action
|
||||||
|
if self.functional_tool_dismantle_id:
|
||||||
|
action = self.functional_tool_dismantle_id.action_open_reference1()
|
||||||
|
return action
|
||||||
elif self.move_id:
|
elif self.move_id:
|
||||||
action = self.move_id.action_open_reference()
|
action = self.move_id.action_open_reference()
|
||||||
if action['res_model'] != 'stock.move':
|
if action['res_model'] != 'stock.move':
|
||||||
@@ -409,13 +411,14 @@ class RealTimeDistributionOfFunctionalTools(models.Model):
|
|||||||
_inherit = ['mail.thread']
|
_inherit = ['mail.thread']
|
||||||
_description = '功能刀具安全库存'
|
_description = '功能刀具安全库存'
|
||||||
|
|
||||||
name = fields.Char('名称', readonly=True, compute='_compute_name', store=True)
|
name = fields.Char('名称', compute='_compute_num', store=True)
|
||||||
functional_name_id = fields.Many2one('sf.tool.inventory', string='功能刀具名称', required=True)
|
functional_name_id = fields.Many2one('sf.tool.inventory', string='功能刀具名称', required=True)
|
||||||
tool_groups_id = fields.Many2one('sf.tool.groups', '刀具组', readonly=False, required=True)
|
tool_groups_id = fields.Many2one('sf.tool.groups', '刀具组', compute='_compute_num', store=True)
|
||||||
sf_cutting_tool_type_id = fields.Many2one('sf.functional.cutting.tool.model', string='功能刀具类型', readonly=False,
|
sf_cutting_tool_type_id = fields.Many2one('sf.functional.cutting.tool.model', string='功能刀具类型',
|
||||||
group_expand='_read_mrs_cutting_tool_type_ids', store=True)
|
compute='_compute_num', store=True,
|
||||||
diameter = fields.Float(string='刀具直径(mm)', readonly=False)
|
group_expand='_read_mrs_cutting_tool_type_ids')
|
||||||
knife_tip_r_angle = fields.Float(string='刀尖R角(mm)', readonly=False)
|
diameter = fields.Float(string='刀具直径(mm)', compute='_compute_num', store=True)
|
||||||
|
knife_tip_r_angle = fields.Float(string='刀尖R角(mm)', compute='_compute_num', store=True)
|
||||||
tool_stock_num = fields.Integer(string='刀具房数量', compute='_compute_stock_num', store=True)
|
tool_stock_num = fields.Integer(string='刀具房数量', compute='_compute_stock_num', store=True)
|
||||||
side_shelf_num = fields.Integer(string='线边刀库数量', compute='_compute_stock_num', store=True)
|
side_shelf_num = fields.Integer(string='线边刀库数量', compute='_compute_stock_num', store=True)
|
||||||
on_tool_stock_num = fields.Integer(string='机内刀库数量', compute='_compute_stock_num', store=True)
|
on_tool_stock_num = fields.Integer(string='机内刀库数量', compute='_compute_stock_num', store=True)
|
||||||
@@ -460,22 +463,18 @@ class RealTimeDistributionOfFunctionalTools(models.Model):
|
|||||||
|
|
||||||
active = fields.Boolean(string='已归档', default=True)
|
active = fields.Boolean(string='已归档', default=True)
|
||||||
|
|
||||||
@api.onchange('functional_name_id')
|
@api.depends('functional_name_id', 'functional_name_id.diameter', 'functional_name_id.angle',
|
||||||
def _onchange_num(self):
|
'functional_name_id.functional_cutting_tool_model_id')
|
||||||
|
def _compute_num(self):
|
||||||
for item in self:
|
for item in self:
|
||||||
if item.functional_name_id:
|
if item.functional_name_id:
|
||||||
item.tool_groups_id = item.functional_name_id.tool_groups_id.id
|
item.tool_groups_id = item.functional_name_id.tool_groups_id.id
|
||||||
item.sf_cutting_tool_type_id = item.functional_name_id.functional_cutting_tool_model_id.id
|
item.sf_cutting_tool_type_id = item.functional_name_id.functional_cutting_tool_model_id.id
|
||||||
item.diameter = item.functional_name_id.diameter
|
item.diameter = item.functional_name_id.diameter
|
||||||
item.knife_tip_r_angle = item.functional_name_id.angle
|
item.knife_tip_r_angle = item.functional_name_id.angle
|
||||||
|
item.name = item.functional_name_id.name
|
||||||
@api.depends('functional_name_id')
|
|
||||||
def _compute_name(self):
|
|
||||||
for obj in self:
|
|
||||||
if obj.tool_groups_id:
|
|
||||||
obj.name = obj.functional_name_id.name
|
|
||||||
else:
|
else:
|
||||||
obj.sudo().name = ''
|
item.sudo().name = ''
|
||||||
|
|
||||||
@api.constrains('min_stock_num', 'max_stock_num')
|
@api.constrains('min_stock_num', 'max_stock_num')
|
||||||
def _check_stock_num(self):
|
def _check_stock_num(self):
|
||||||
@@ -583,4 +582,10 @@ class RealTimeDistributionOfFunctionalTools(models.Model):
|
|||||||
for vals in vals_list:
|
for vals in vals_list:
|
||||||
vals['status_create'] = False
|
vals['status_create'] = False
|
||||||
records = super(RealTimeDistributionOfFunctionalTools, self).create(vals_list)
|
records = super(RealTimeDistributionOfFunctionalTools, self).create(vals_list)
|
||||||
|
for item in records:
|
||||||
|
if item:
|
||||||
|
record = self.search([('functional_name_id', '=', item.functional_name_id.id)])
|
||||||
|
if len(record) > 1:
|
||||||
|
raise ValidationError(
|
||||||
|
'功能刀具名称为【%s】的安全库存已经存在,请勿重复创建!!!' % item.functional_name_id.name)
|
||||||
return records
|
return records
|
||||||
|
|||||||
@@ -37,14 +37,10 @@ class ToolDatasync(models.Model):
|
|||||||
|
|
||||||
def _cron_tool_datasync_all(self):
|
def _cron_tool_datasync_all(self):
|
||||||
try:
|
try:
|
||||||
self.env['stock.lot'].sudo().sync_enroll_tool_material_stock_all()
|
|
||||||
|
|
||||||
self.env['stock.lot'].sudo().sync_enroll_fixture_material_stock_all()
|
|
||||||
|
|
||||||
self.env['sf.tool.material.search'].sudo().sync_enroll_tool_material_all()
|
self.env['sf.tool.material.search'].sudo().sync_enroll_tool_material_all()
|
||||||
|
self.env['stock.lot'].sudo().sync_enroll_tool_material_stock_all()
|
||||||
self.env['sf.fixture.material.search'].sudo().sync_enroll_fixture_material_all()
|
self.env['sf.fixture.material.search'].sudo().sync_enroll_fixture_material_all()
|
||||||
|
self.env['stock.lot'].sudo().sync_enroll_fixture_material_stock_all()
|
||||||
self.env['sf.functional.cutting.tool.entity'].sudo().esync_enroll_functional_tool_entity_all()
|
self.env['sf.functional.cutting.tool.entity'].sudo().esync_enroll_functional_tool_entity_all()
|
||||||
logging.info("已全部同步完成!!!")
|
logging.info("已全部同步完成!!!")
|
||||||
# self.env['sf.functional.tool.warning'].sudo().sync_enroll_functional_tool_warning_all()
|
# self.env['sf.functional.tool.warning'].sudo().sync_enroll_functional_tool_warning_all()
|
||||||
@@ -106,7 +102,7 @@ class StockLot(models.Model):
|
|||||||
logging.info("没有刀具物料序列号信息")
|
logging.info("没有刀具物料序列号信息")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info("刀具物料序列号同步失败:%s" % e)
|
logging.info("刀具物料序列号同步失败:%s" % e)
|
||||||
|
|
||||||
|
|
||||||
class ToolMaterial(models.Model):
|
class ToolMaterial(models.Model):
|
||||||
_inherit = 'sf.tool.material.search'
|
_inherit = 'sf.tool.material.search'
|
||||||
@@ -198,7 +194,7 @@ class FunctionalCuttingToolEntity(models.Model):
|
|||||||
for item in objs_all:
|
for item in objs_all:
|
||||||
val = {
|
val = {
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'code': item.code,
|
'code': False if not item.code else item.code.split('-', 1)[1],
|
||||||
'name': item.name,
|
'name': item.name,
|
||||||
'rfid': item.rfid,
|
'rfid': item.rfid,
|
||||||
'tool_groups_name': item.tool_groups_id.name,
|
'tool_groups_name': item.tool_groups_id.name,
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
<field name="current_location" string="当前位置"/>
|
<field name="current_location" string="当前位置"/>
|
||||||
|
|
||||||
<field name="current_location_id" invisible="1"/>
|
<field name="current_location_id" invisible="1"/>
|
||||||
<field name="current_location" optional="hide"/>
|
|
||||||
<field name="sf_cutting_tool_type_id" invisible="True"/>
|
<field name="sf_cutting_tool_type_id" invisible="True"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
@@ -61,13 +60,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="oe_stat_button" groups="sf_base.group_sf_mrp_user"
|
<button class="oe_stat_button" groups="sf_base.group_sf_mrp_user"
|
||||||
name="open_stock_move_line"
|
name="open_stock_move_line"
|
||||||
icon="fa-list-ul"
|
|
||||||
type="object">
|
type="object">
|
||||||
<div class="o_field_widget o_stat_info">
|
<i class="fa fa-fw o_button_icon fa-exchange"/>
|
||||||
<span>
|
<span>出库入库记录</span>
|
||||||
出库入库记录
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
<button class="oe_stat_button" groups="sf_base.group_sf_mrp_user"
|
<button class="oe_stat_button" groups="sf_base.group_sf_mrp_user"
|
||||||
name="open_safety_stock"
|
name="open_safety_stock"
|
||||||
@@ -174,7 +169,7 @@
|
|||||||
<field name="cut_time" attrs="{'invisible': [('new_former','=','0')]}"/>
|
<field name="cut_time" attrs="{'invisible': [('new_former','=','0')]}"/>
|
||||||
<field name="cut_length" attrs="{'invisible': [('new_former','=','0')]}"/>
|
<field name="cut_length" attrs="{'invisible': [('new_former','=','0')]}"/>
|
||||||
<field name="cut_number" attrs="{'invisible': [('new_former','=','0')]}"/>
|
<field name="cut_number" attrs="{'invisible': [('new_former','=','0')]}"/>
|
||||||
<field name="current_location_id" string="当前位置"/>
|
<field name="current_location_id" string="当前位置" invisible="1"/>
|
||||||
<field name="current_location" string="当前位置"/>
|
<field name="current_location" string="当前位置"/>
|
||||||
<field name="current_shelf_location_id" string="当前货位"
|
<field name="current_shelf_location_id" string="当前货位"
|
||||||
attrs="{'invisible': [('current_shelf_location_id', '=', False)]}"/>
|
attrs="{'invisible': [('current_shelf_location_id', '=', False)]}"/>
|
||||||
@@ -482,18 +477,19 @@
|
|||||||
<field name="reference" string="单据号"/>
|
<field name="reference" string="单据号"/>
|
||||||
<field name="lot_id" invisible="1"/>
|
<field name="lot_id" invisible="1"/>
|
||||||
<field name="rfid"/>
|
<field name="rfid"/>
|
||||||
<field name="functional_tool_name_id" optional="hide"/>
|
|
||||||
<field name="functional_tool_name" string="功能刀具名称"/>
|
<field name="functional_tool_name" string="功能刀具名称"/>
|
||||||
<field name="diameter"/>
|
<field name="diameter"/>
|
||||||
<field name="knife_tip_r_angle"/>
|
<field name="knife_tip_r_angle"/>
|
||||||
<field name="install_tool_time"/>
|
|
||||||
<field name="location_id"/>
|
<field name="location_id"/>
|
||||||
<field name="current_location_id"/>
|
<field name="current_location_id"/>
|
||||||
<field name="location_dest_id"/>
|
<field name="location_dest_id"/>
|
||||||
<field name="destination_location_id"/>
|
<field name="destination_location_id"/>
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
<field name="qty_done" string="数量"/>
|
<field name="qty_done" string="数量"/>
|
||||||
<field name="functional_tool_type_id" invisible="True"/>
|
<field name="functional_tool_type_id" invisible="1"/>
|
||||||
|
<field name="functional_tool_name_id" invisible="1"/>
|
||||||
|
<field name="functional_tool_dismantle_id" invisible="1"/>
|
||||||
|
<field name="install_tool_time" invisible="1"/>
|
||||||
<!-- <button name="enroll_functional_tool_move" string="安全库存注册" type="object" class="btn-primary"/>-->
|
<!-- <button name="enroll_functional_tool_move" string="安全库存注册" type="object" class="btn-primary"/>-->
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
|
|||||||
@@ -407,6 +407,7 @@
|
|||||||
<field name="plan_execute_status"/>
|
<field name="plan_execute_status"/>
|
||||||
<filter string="已归档" name="inactive" domain="[('active', '=', False)]"/>
|
<filter string="已归档" name="inactive" domain="[('active', '=', False)]"/>
|
||||||
<searchpanel>
|
<searchpanel>
|
||||||
|
<field name="plan_execute_status" string="状态" enable_counters="1" icon="fa-filter"/>
|
||||||
<field name="production_line_id" string="生产线" enable_counters="1" icon="fa-filter"/>
|
<field name="production_line_id" string="生产线" enable_counters="1" icon="fa-filter"/>
|
||||||
<field name="functional_tool_type_id" string="功能刀具类型" enable_counters="1"
|
<field name="functional_tool_type_id" string="功能刀具类型" enable_counters="1"
|
||||||
icon="fa-filter"/>
|
icon="fa-filter"/>
|
||||||
@@ -476,6 +477,17 @@
|
|||||||
<field name="assemble_status" widget="statusbar" statusbar_visible="0,1"/>
|
<field name="assemble_status" widget="statusbar" statusbar_visible="0,1"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button class="oe_stat_button" name="open_tool_stock_picking" icon="fa-truck" type="object"
|
||||||
|
attrs="{'invisible': [('assemble_status', '!=', '1')]}">
|
||||||
|
<div name="delivery_count" class="o_field_widget o_readonly_modifier o_field_statinfo">
|
||||||
|
<span class="o_stat_info o_stat_value">
|
||||||
|
<field name="picking_num"/>
|
||||||
|
</span>
|
||||||
|
<span class="o_stat_text">调拨</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<h1>
|
<h1>
|
||||||
<field name="assembly_order_code"/>
|
<field name="assembly_order_code"/>
|
||||||
@@ -711,10 +723,14 @@
|
|||||||
</searchpanel>
|
</searchpanel>
|
||||||
|
|
||||||
<group expand="0" string="Group By...">
|
<group expand="0" string="Group By...">
|
||||||
<filter string="功能刀具名称" name="name" domain="[]" context="{'group_by': 'functional_tool_name'}"/>
|
<filter string="功能刀具名称" name="name" domain="[]"
|
||||||
<filter string="刀具组" name="tool_groups" domain="[]" context="{'group_by': 'tool_groups_id'}"/>
|
context="{'group_by': 'functional_tool_name'}"/>
|
||||||
<filter string="任务来源" name="loading_task_source" domain="[]" context="{'group_by': 'loading_task_source'}"/>
|
<filter string="刀具组" name="tool_groups" domain="[]"
|
||||||
<filter string="用刀时间" name="use_tool_time" domain="[]" context="{'group_by': 'use_tool_time'}"/>
|
context="{'group_by': 'tool_groups_id'}"/>
|
||||||
|
<filter string="任务来源" name="loading_task_source" domain="[]"
|
||||||
|
context="{'group_by': 'loading_task_source'}"/>
|
||||||
|
<filter string="用刀时间" name="use_tool_time" domain="[]"
|
||||||
|
context="{'group_by': 'use_tool_time'}"/>
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</field>
|
||||||
@@ -759,10 +775,25 @@
|
|||||||
<form>
|
<form>
|
||||||
<header>
|
<header>
|
||||||
<button string="确认拆解" name="confirmation_disassembly" type="object" class="btn-primary"
|
<button string="确认拆解" name="confirmation_disassembly" type="object" class="btn-primary"
|
||||||
confirm="是否确认拆解" attrs="{'invisible': [('state', '=', '已拆解')]}"/>
|
confirm="是否确认拆解" attrs="{'invisible': [('state', '!=', '待拆解')]}"/>
|
||||||
<field name="state" widget="statusbar" statusbar_visible="待拆解,已拆解"/>
|
<field name="state" widget="statusbar" statusbar_visible="待拆解,已拆解"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button class="oe_stat_button" name="open_tool_stock_picking" icon="fa-truck" type="object"
|
||||||
|
attrs="{'invisible':[('state', '!=', '已拆解')]}">
|
||||||
|
<div name="delivery_count" class="o_field_widget o_readonly_modifier o_field_statinfo">
|
||||||
|
<span class="o_stat_info o_stat_value">
|
||||||
|
<field name="picking_num"/>
|
||||||
|
</span>
|
||||||
|
<span class="o_stat_text">调拨</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="oe_stat_button" name="open_function_tool_stock_move_line" icon="fa-exchange"
|
||||||
|
type="object">
|
||||||
|
<span>功能刀具移动</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<h1>
|
<h1>
|
||||||
<field name="code"/>
|
<field name="code"/>
|
||||||
@@ -773,12 +804,13 @@
|
|||||||
<group>
|
<group>
|
||||||
<field name="functional_tool_id" placeholder="请选择将要拆解的功能刀具"
|
<field name="functional_tool_id" placeholder="请选择将要拆解的功能刀具"
|
||||||
options="{'no_create': True}" attrs="{'readonly': [('state', '=', '已拆解')]}"/>
|
options="{'no_create': True}" attrs="{'readonly': [('state', '=', '已拆解')]}"/>
|
||||||
<field name="rfid" attrs="{'invisible': [('state', '=', '已拆解')]}"/>
|
<field name="rfid" attrs="{'invisible': [('rfid', '=', '')]}"/>
|
||||||
<field name="rfid_dismantle" attrs="{'invisible': [('state', '!=', '已拆解')]}"/>
|
<field name="rfid_dismantle" attrs="{'invisible': [('rfid_dismantle', '=', False)]}"/>
|
||||||
<field name="tool_type_id"/>
|
<field name="tool_type_id"/>
|
||||||
<field name="tool_groups_id"/>
|
<field name="tool_groups_id"/>
|
||||||
<field name="diameter"/>
|
<field name="diameter"/>
|
||||||
<field name="knife_tip_r_angle"/>
|
<field name="knife_tip_r_angle"/>
|
||||||
|
<field name="picking_id" invisible="1"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="image"/>
|
<field name="image"/>
|
||||||
@@ -791,8 +823,8 @@
|
|||||||
attrs="{'readonly': [('state', '=', '已拆解')]}"/>
|
attrs="{'readonly': [('state', '=', '已拆解')]}"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="scrap_id"
|
<!-- <field name="scrap_id"-->
|
||||||
attrs="{'invisible': [('dismantle_cause', 'not in', ['寿命到期报废','崩刀报废'])]}"/>
|
<!-- attrs="{'invisible': [('dismantle_cause', 'not in', ['寿命到期报废','崩刀报废'])]}"/>-->
|
||||||
<field name="grinding_id"
|
<field name="grinding_id"
|
||||||
attrs="{'invisible': [('dismantle_cause', 'not in', ['刀具需磨削'])]}"/>
|
attrs="{'invisible': [('dismantle_cause', 'not in', ['刀具需磨削'])]}"/>
|
||||||
</group>
|
</group>
|
||||||
@@ -879,6 +911,23 @@
|
|||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
|
<page string="报废"
|
||||||
|
attrs="{'invisible':[('dismantle_cause', 'not in', ['寿命到期报废','崩刀报废'])]}">
|
||||||
|
<field name="scrap_ids">
|
||||||
|
<tree>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="lot_id"/>
|
||||||
|
<field name="location_id"/>
|
||||||
|
<field name="scrap_location_id"/>
|
||||||
|
<field name="scrap_qty"/>
|
||||||
|
<field name="product_uom_id"/>
|
||||||
|
<field name="date_done"/>
|
||||||
|
<field name="state" widget="badge" decoration-success="state == 'done'"
|
||||||
|
decoration-muted="state == 'draft'"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
<page string="其他">
|
<page string="其他">
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
|
|||||||
@@ -621,26 +621,10 @@ class FunctionalToolAssemblyOrder(models.TransientModel):
|
|||||||
desc_1 = self.get_desc_1(stock_lot)
|
desc_1 = self.get_desc_1(stock_lot)
|
||||||
# 封装功能刀具数据,用于创建功能刀具记录
|
# 封装功能刀具数据,用于创建功能刀具记录
|
||||||
desc_2 = self.get_desc_2(stock_lot, functional_tool_assembly)
|
desc_2 = self.get_desc_2(stock_lot, functional_tool_assembly)
|
||||||
# 创建刀具组装入库单
|
# 创建功能刀具组装入库单
|
||||||
self.env['stock.picking'].create_stocking_picking(stock_lot, functional_tool_assembly, self)
|
self.env['stock.picking'].create_tool_stocking_picking(stock_lot, functional_tool_assembly, self)
|
||||||
# 刀具物料出库
|
# 创建刀具物料出库单
|
||||||
if self.handle_code_id:
|
self.env['stock.picking'].create_tool_stocking_picking1(self)
|
||||||
product_id.tool_material_stock_moves(self.handle_code_id, self.assembly_order_code)
|
|
||||||
if self.integral_product_id:
|
|
||||||
self.integral_product_id.material_stock_moves(self.integral_freight_barcode_id,
|
|
||||||
self.integral_freight_lot_id, self.assembly_order_code)
|
|
||||||
if self.blade_product_id:
|
|
||||||
self.blade_product_id.material_stock_moves(self.blade_freight_barcode_id,
|
|
||||||
self.blade_freight_lot_id, self.assembly_order_code)
|
|
||||||
if self.bar_product_id:
|
|
||||||
self.bar_product_id.material_stock_moves(self.bar_freight_barcode_id,
|
|
||||||
self.bar_freight_lot_id, self.assembly_order_code)
|
|
||||||
if self.pad_product_id:
|
|
||||||
self.pad_product_id.material_stock_moves(self.pad_freight_barcode_id,
|
|
||||||
self.pad_freight_lot_id, self.assembly_order_code)
|
|
||||||
if self.chuck_product_id:
|
|
||||||
self.chuck_product_id.material_stock_moves(self.chuck_freight_barcode_id,
|
|
||||||
self.chuck_freight_lot_id, self.assembly_order_code)
|
|
||||||
|
|
||||||
# ============================创建功能刀具列表、安全库存记录===============================
|
# ============================创建功能刀具列表、安全库存记录===============================
|
||||||
# 创建功能刀具列表记录
|
# 创建功能刀具列表记录
|
||||||
@@ -786,9 +770,9 @@ class FunctionalToolAssemblyOrder(models.TransientModel):
|
|||||||
class StockPicking(models.Model):
|
class StockPicking(models.Model):
|
||||||
_inherit = 'stock.picking'
|
_inherit = 'stock.picking'
|
||||||
|
|
||||||
def create_stocking_picking(self, stock_lot, functional_tool_assembly, obj):
|
def create_tool_stocking_picking(self, stock_lot, functional_tool_assembly, obj):
|
||||||
"""
|
"""
|
||||||
创建刀具组装入库单
|
创建功能刀具组装入库单
|
||||||
"""
|
"""
|
||||||
# 获取名称为刀具组装入库的作业类型
|
# 获取名称为刀具组装入库的作业类型
|
||||||
picking_type_id = self.env['stock.picking.type'].sudo().search([('name', '=', '刀具组装入库')])
|
picking_type_id = self.env['stock.picking.type'].sudo().search([('name', '=', '刀具组装入库')])
|
||||||
@@ -807,6 +791,7 @@ class StockPicking(models.Model):
|
|||||||
'location_id': picking_id.location_id.id,
|
'location_id': picking_id.location_id.id,
|
||||||
'location_dest_id': picking_id.location_dest_id.id,
|
'location_dest_id': picking_id.location_dest_id.id,
|
||||||
'lot_id': stock_lot.id,
|
'lot_id': stock_lot.id,
|
||||||
|
'install_tool_time': fields.Datetime.now(),
|
||||||
'qty_done': 1,
|
'qty_done': 1,
|
||||||
'functional_tool_name_id': functional_tool_assembly.id,
|
'functional_tool_name_id': functional_tool_assembly.id,
|
||||||
'functional_tool_type_id': obj.functional_tool_type_id.id,
|
'functional_tool_type_id': obj.functional_tool_type_id.id,
|
||||||
@@ -836,6 +821,101 @@ class StockPicking(models.Model):
|
|||||||
num = "%03d" % m
|
num = "%03d" % m
|
||||||
return name + str(num)
|
return name + str(num)
|
||||||
|
|
||||||
|
def create_tool_stocking_picking1(self, obj):
|
||||||
|
"""
|
||||||
|
创建刀具物料出库单
|
||||||
|
"""
|
||||||
|
# 获取名称为内部调拨的作业类型
|
||||||
|
picking_type_id = self.env['stock.picking.type'].sudo().search([('name', '=', '内部调拨')])
|
||||||
|
# 创建刀具物料出库单
|
||||||
|
picking_id = self.env['stock.picking'].create({
|
||||||
|
'name': self._get_name_stock1(picking_type_id),
|
||||||
|
'picking_type_id': picking_type_id.id,
|
||||||
|
'location_id': self.env['stock.location'].search([('name', '=', '刀具房')]).id,
|
||||||
|
'location_dest_id': self.env['stock.location'].search([('name', '=', '刀具组装位置')]).id,
|
||||||
|
'origin': obj.assembly_order_code
|
||||||
|
})
|
||||||
|
# =============刀具物料出库===================
|
||||||
|
stock_move_id = self.env['stock.move']
|
||||||
|
datas = {'data': [], 'picking_id': picking_id}
|
||||||
|
if obj.handle_code_id:
|
||||||
|
datas['data'].append(
|
||||||
|
{'current_location_id': self.env['sf.shelf.location'], 'lot_id': obj.handle_code_id})
|
||||||
|
if obj.integral_product_id:
|
||||||
|
datas['data'].append(
|
||||||
|
{'current_location_id': obj.integral_freight_barcode_id, 'lot_id': obj.integral_freight_lot_id.lot_id})
|
||||||
|
if obj.blade_product_id:
|
||||||
|
datas['data'].append(
|
||||||
|
{'current_location_id': obj.blade_freight_barcode_id, 'lot_id': obj.blade_freight_lot_id.lot_id})
|
||||||
|
if obj.bar_product_id:
|
||||||
|
datas['data'].append(
|
||||||
|
{'current_location_id': obj.bar_freight_barcode_id, 'lot_id': obj.bar_freight_lot_id.lot_id})
|
||||||
|
if obj.pad_product_id:
|
||||||
|
datas['data'].append(
|
||||||
|
{'current_location_id': obj.pad_freight_barcode_id, 'lot_id': obj.pad_freight_lot_id.lot_id})
|
||||||
|
if obj.chuck_product_id:
|
||||||
|
datas['data'].append(
|
||||||
|
{'current_location_id': obj.chuck_freight_barcode_id, 'lot_id': obj.chuck_freight_lot_id.lot_id})
|
||||||
|
# 创建刀具物料出库库存移动记录
|
||||||
|
stock_move_id.create_tool_material_stock_moves(datas)
|
||||||
|
# 将刀具物料出库库单的状态更改为就绪
|
||||||
|
picking_id.action_confirm()
|
||||||
|
# 修改刀具物料出库移动历史记录
|
||||||
|
stock_move_id.write_tool_material_stock_move_lines(datas)
|
||||||
|
# 设置数量,并验证完成
|
||||||
|
picking_id.action_set_quantities_to_reservation()
|
||||||
|
picking_id.button_validate()
|
||||||
|
|
||||||
|
def _get_name_stock1(self, picking_type_id):
|
||||||
|
name = picking_type_id.sequence_id.prefix
|
||||||
|
stock_id = self.env['stock.picking'].sudo().search(
|
||||||
|
[('name', 'like', name), ('picking_type_id', '=', picking_type_id.id)],
|
||||||
|
limit=1,
|
||||||
|
order="id desc"
|
||||||
|
)
|
||||||
|
if not stock_id:
|
||||||
|
num = "%05d" % 1
|
||||||
|
else:
|
||||||
|
m = int(stock_id.name[-3:]) + 1
|
||||||
|
num = "%05d" % m
|
||||||
|
return name + str(num)
|
||||||
|
|
||||||
|
|
||||||
|
class StockMove(models.Model):
|
||||||
|
_inherit = 'stock.move'
|
||||||
|
|
||||||
|
def create_tool_material_stock_moves(self, datas):
|
||||||
|
picking_id = datas['picking_id']
|
||||||
|
data = datas['data']
|
||||||
|
stock_move_ids = []
|
||||||
|
for res in data:
|
||||||
|
if res:
|
||||||
|
# 创建库存移动记录
|
||||||
|
stock_move_id = self.env['stock.move'].sudo().create({
|
||||||
|
'name': picking_id.name,
|
||||||
|
'picking_id': picking_id.id,
|
||||||
|
'product_id': res['lot_id'].product_id.id,
|
||||||
|
'location_id': picking_id.location_id.id,
|
||||||
|
'location_dest_id': picking_id.location_dest_id.id,
|
||||||
|
'product_uom_qty': 1.00,
|
||||||
|
'reserved_availability': 1.00
|
||||||
|
})
|
||||||
|
stock_move_ids.append(stock_move_id)
|
||||||
|
return stock_move_ids
|
||||||
|
|
||||||
|
def write_tool_material_stock_move_lines(self, datas):
|
||||||
|
picking_id = datas['picking_id']
|
||||||
|
data = datas['data']
|
||||||
|
move_line_ids = picking_id.move_line_ids
|
||||||
|
for move_line_id in move_line_ids:
|
||||||
|
for res in data:
|
||||||
|
if move_line_id.lot_id.product_id == res['lot_id'].product_id:
|
||||||
|
move_line_id.write({
|
||||||
|
'current_location_id': res.get('current_location_id').id,
|
||||||
|
'lot_id': res.get('lot_id').id
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ProductProduct(models.Model):
|
class ProductProduct(models.Model):
|
||||||
_inherit = 'product.product'
|
_inherit = 'product.product'
|
||||||
@@ -853,13 +933,6 @@ class ProductProduct(models.Model):
|
|||||||
'product_id': product_id[0].id,
|
'product_id': product_id[0].id,
|
||||||
'company_id': self.env.company.id
|
'company_id': self.env.company.id
|
||||||
})
|
})
|
||||||
# 获取位置对象
|
|
||||||
location_inventory_ids = self.env['stock.location'].search([('name', 'in', ('Production', '生产'))])
|
|
||||||
stock_location_id = self.env['stock.location'].search([('name', '=', '组装后')])
|
|
||||||
# 创建功能刀具该批次/序列号 库存移动和移动历史
|
|
||||||
stock_lot.create_stock_quant(location_inventory_ids[-1], stock_location_id, functional_tool_assembly.id,
|
|
||||||
obj.assembly_order_code, obj, obj.after_tool_groups_id)
|
|
||||||
|
|
||||||
return stock_lot
|
return stock_lot
|
||||||
|
|
||||||
def get_stock_lot_name(self, obj):
|
def get_stock_lot_name(self, obj):
|
||||||
@@ -877,87 +950,3 @@ class ProductProduct(models.Model):
|
|||||||
m = int(stock_lot_id.name[-3:]) + 1
|
m = int(stock_lot_id.name[-3:]) + 1
|
||||||
num = "%03d" % m
|
num = "%03d" % m
|
||||||
return '%s-%s' % (code, num)
|
return '%s-%s' % (code, num)
|
||||||
|
|
||||||
def tool_material_stock_moves(self, tool_material, assembly_order_code):
|
|
||||||
"""
|
|
||||||
对刀具物料进行库存移动到 刀具组装位置
|
|
||||||
"""
|
|
||||||
# 获取位置对象
|
|
||||||
location_inventory_id = tool_material.quant_ids.location_id[-1]
|
|
||||||
stock_location_id = self.env['stock.location'].search([('name', '=', '刀具组装位置')])
|
|
||||||
# 创建功能刀具该批次/序列号 库存移动和移动历史
|
|
||||||
tool_material.create_stock_quant(location_inventory_id, stock_location_id, None, assembly_order_code, False,
|
|
||||||
False)
|
|
||||||
|
|
||||||
def material_stock_moves(self, shelf_location_barcode_id, lot_id, assembly_order_code):
|
|
||||||
# 创建库存移动记录
|
|
||||||
stock_move_id = self.env['stock.move'].sudo().create({
|
|
||||||
'name': assembly_order_code,
|
|
||||||
'product_id': self.id,
|
|
||||||
'location_id': self.env['stock.location'].search([('name', '=', '刀具房')]).id,
|
|
||||||
'location_dest_id': self.env['stock.location'].search([('name', '=', '刀具组装位置')]).id,
|
|
||||||
'product_uom_qty': 1.00,
|
|
||||||
'state': 'done'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 创建移动历史记录
|
|
||||||
stock_move_line_id = self.env['stock.move.line'].sudo().create({
|
|
||||||
'product_id': self.id,
|
|
||||||
'move_id': stock_move_id.id,
|
|
||||||
'lot_id': lot_id.lot_id.id,
|
|
||||||
'current_location_id': shelf_location_barcode_id.id,
|
|
||||||
'install_tool_time': fields.Datetime.now(),
|
|
||||||
'qty_done': 1.0,
|
|
||||||
'state': 'done',
|
|
||||||
})
|
|
||||||
return stock_move_id, stock_move_line_id
|
|
||||||
|
|
||||||
|
|
||||||
class StockLot(models.Model):
|
|
||||||
_inherit = 'stock.lot'
|
|
||||||
|
|
||||||
def create_stock_quant(self, location_inventory_id, stock_location_id, functional_tool_assembly_id, name, obj,
|
|
||||||
tool_groups_id):
|
|
||||||
"""
|
|
||||||
对功能刀具组装过程的功能刀具和刀具物料进行库存移动,以及创建移动历史
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 创建库存移动记录
|
|
||||||
stock_move_id = self.env['stock.move'].sudo().create({
|
|
||||||
'name': name,
|
|
||||||
'product_id': self.product_id.id,
|
|
||||||
'location_id': location_inventory_id.id,
|
|
||||||
'location_dest_id': stock_location_id.id,
|
|
||||||
'product_uom_qty': 1.00,
|
|
||||||
'state': 'done'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 创建移动历史记录
|
|
||||||
stock_move_line_id = self.env['stock.move.line'].sudo().create({
|
|
||||||
'product_id': self.product_id.id,
|
|
||||||
'functional_tool_name_id': functional_tool_assembly_id,
|
|
||||||
'lot_id': self.id,
|
|
||||||
'move_id': stock_move_id.id,
|
|
||||||
'install_tool_time': fields.Datetime.now(),
|
|
||||||
'qty_done': 1.0,
|
|
||||||
'state': 'done',
|
|
||||||
'functional_tool_type_id': False if not obj else obj.functional_tool_type_id.id,
|
|
||||||
'diameter': None if not obj else obj.after_assembly_functional_tool_diameter,
|
|
||||||
'knife_tip_r_angle': None if not obj else obj.after_assembly_knife_tip_r_angle,
|
|
||||||
'code': '' if not obj else obj.code,
|
|
||||||
'rfid': '' if not obj else obj.rfid,
|
|
||||||
'functional_tool_name': '' if not obj else obj.after_assembly_functional_tool_name,
|
|
||||||
'tool_groups_id': False if not tool_groups_id else tool_groups_id.id
|
|
||||||
})
|
|
||||||
return stock_move_id, stock_move_line_id
|
|
||||||
|
|
||||||
# class StockQuant(models.Model):
|
|
||||||
# _inherit = 'stock.quant'
|
|
||||||
#
|
|
||||||
# @api.model_create_multi
|
|
||||||
# def create(self, vals_list):
|
|
||||||
# records = super(StockQuant, self).create(vals_list)
|
|
||||||
# for record in records:
|
|
||||||
# if record.lot_id.product_id.categ_id.name == '刀具':
|
|
||||||
# record.lot_id.enroll_tool_material_stock()
|
|
||||||
# return records
|
|
||||||
|
|||||||
138
sg_wechat_enterprise/.gitignore
vendored
Normal file
138
sg_wechat_enterprise/.gitignore
vendored
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# 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/
|
||||||
3
sg_wechat_enterprise/__init__.py
Normal file
3
sg_wechat_enterprise/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from . import we_api
|
||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
52
sg_wechat_enterprise/__manifest__.py
Normal file
52
sg_wechat_enterprise/__manifest__.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- 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',
|
||||||
|
}
|
||||||
273
sg_wechat_enterprise/controllers/WXBizMsgCrypt.py
Normal file
273
sg_wechat_enterprise/controllers/WXBizMsgCrypt.py
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
#!/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:成功0,sEncryptMsg,失败返回对应的错误码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
|
||||||
3
sg_wechat_enterprise/controllers/__init__.py
Normal file
3
sg_wechat_enterprise/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
from . import wechat_enterprise
|
||||||
|
|
||||||
20
sg_wechat_enterprise/controllers/ierror.py
Normal file
20
sg_wechat_enterprise/controllers/ierror.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#########################################################################
|
||||||
|
# Author: jonyqin
|
||||||
|
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
|
||||||
|
# File Name: ierror.py
|
||||||
|
# Description:定义错误码含义
|
||||||
|
#########################################################################
|
||||||
|
WXBizMsgCrypt_OK = 0
|
||||||
|
WXBizMsgCrypt_ValidateSignature_Error = -40001
|
||||||
|
WXBizMsgCrypt_ParseXml_Error = -40002
|
||||||
|
WXBizMsgCrypt_ComputeSignature_Error = -40003
|
||||||
|
WXBizMsgCrypt_IllegalAesKey = -40004
|
||||||
|
WXBizMsgCrypt_ValidateCorpid_Error = -40005
|
||||||
|
WXBizMsgCrypt_EncryptAES_Error = -40006
|
||||||
|
WXBizMsgCrypt_DecryptAES_Error = -40007
|
||||||
|
WXBizMsgCrypt_IllegalBuffer = -40008
|
||||||
|
WXBizMsgCrypt_EncodeBase64_Error = -40009
|
||||||
|
WXBizMsgCrypt_DecodeBase64_Error = -40010
|
||||||
|
WXBizMsgCrypt_GenReturnXml_Error = -40011
|
||||||
231
sg_wechat_enterprise/controllers/wechat_enterprise.py
Normal file
231
sg_wechat_enterprise/controllers/wechat_enterprise.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
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)
|
||||||
16
sg_wechat_enterprise/data/data.xml
Normal file
16
sg_wechat_enterprise/data/data.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
13
sg_wechat_enterprise/demo/we_config_demo.xml
Normal file
13
sg_wechat_enterprise/demo/we_config_demo.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
9
sg_wechat_enterprise/models/__init__.py
Normal file
9
sg_wechat_enterprise/models/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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
|
||||||
11
sg_wechat_enterprise/models/client/__init__.py
Normal file
11
sg_wechat_enterprise/models/client/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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()
|
||||||
2
sg_wechat_enterprise/models/client/api/__init__.py
Normal file
2
sg_wechat_enterprise/models/client/api/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .jkm_user import JkmWechatUser
|
||||||
|
from .jkm_oauth import JkmWechatOauth
|
||||||
53
sg_wechat_enterprise/models/client/api/jkm_oauth.py
Normal file
53
sg_wechat_enterprise/models/client/api/jkm_oauth.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
21
sg_wechat_enterprise/models/client/api/jkm_user.py
Normal file
21
sg_wechat_enterprise/models/client/api/jkm_user.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
121
sg_wechat_enterprise/models/mail.py
Normal file
121
sg_wechat_enterprise/models/mail.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# -*- 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全称,如>
|
||||||
|
key=sz.group('name')#去除&;后entity,如>为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
|
||||||
26
sg_wechat_enterprise/models/res_users.py
Normal file
26
sg_wechat_enterprise/models/res_users.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# -*- 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
|
||||||
213
sg_wechat_enterprise/models/we_app.py
Normal file
213
sg_wechat_enterprise/models/we_app.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
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
|
||||||
62
sg_wechat_enterprise/models/we_conf.py
Normal file
62
sg_wechat_enterprise/models/we_conf.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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)
|
||||||
143
sg_wechat_enterprise/models/we_receive_message.py
Normal file
143
sg_wechat_enterprise/models/we_receive_message.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
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))
|
||||||
109
sg_wechat_enterprise/models/we_receive_message_process.py
Normal file
109
sg_wechat_enterprise/models/we_receive_message_process.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
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))
|
||||||
108
sg_wechat_enterprise/models/we_send_message.py
Normal file
108
sg_wechat_enterprise/models/we_send_message.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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"初始化企业号失败")
|
||||||
89
sg_wechat_enterprise/models/we_tools.py
Normal file
89
sg_wechat_enterprise/models/we_tools.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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)
|
||||||
1
sg_wechat_enterprise/requirements.txt
Normal file
1
sg_wechat_enterprise/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
wechatpy==1.8.6
|
||||||
13
sg_wechat_enterprise/security/ir.model.access.csv
Normal file
13
sg_wechat_enterprise/security/ir.model.access.csv
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
|
24
sg_wechat_enterprise/static/css/loading.css
Normal file
24
sg_wechat_enterprise/static/css/loading.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
@-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;
|
||||||
|
}
|
||||||
BIN
sg_wechat_enterprise/static/description/icon.png
Normal file
BIN
sg_wechat_enterprise/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
sg_wechat_enterprise/static/description/qyh.png
Normal file
BIN
sg_wechat_enterprise/static/description/qyh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
26
sg_wechat_enterprise/static/js/url_transfers.js
Normal file
26
sg_wechat_enterprise/static/js/url_transfers.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
1
sg_wechat_enterprise/tests/__init__.py
Normal file
1
sg_wechat_enterprise/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
84
sg_wechat_enterprise/views/app_view.xml
Normal file
84
sg_wechat_enterprise/views/app_view.xml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
23
sg_wechat_enterprise/views/mail_view.xml
Normal file
23
sg_wechat_enterprise/views/mail_view.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?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>
|
||||||
34
sg_wechat_enterprise/views/menu_view.xml
Normal file
34
sg_wechat_enterprise/views/menu_view.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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>
|
||||||
20
sg_wechat_enterprise/views/res_users_view.xml
Normal file
20
sg_wechat_enterprise/views/res_users_view.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
149
sg_wechat_enterprise/views/we_app_view.xml
Normal file
149
sg_wechat_enterprise/views/we_app_view.xml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
48
sg_wechat_enterprise/views/we_config_view.xml
Normal file
48
sg_wechat_enterprise/views/we_config_view.xml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
38
sg_wechat_enterprise/views/we_menu.xml
Normal file
38
sg_wechat_enterprise/views/we_menu.xml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?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>
|
||||||
70
sg_wechat_enterprise/views/we_message_process_view.xml
Normal file
70
sg_wechat_enterprise/views/we_message_process_view.xml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
98
sg_wechat_enterprise/views/we_receive_message_view.xml
Normal file
98
sg_wechat_enterprise/views/we_receive_message_view.xml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
105
sg_wechat_enterprise/views/we_send_message_view.xml
Normal file
105
sg_wechat_enterprise/views/we_send_message_view.xml
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
45
sg_wechat_enterprise/views/we_templates.xml
Normal file
45
sg_wechat_enterprise/views/we_templates.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<template id="sg_wechat_enterprise.layout" name="Wechat_Enterprise_Layout">
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<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_Warning">
|
||||||
|
<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>
|
||||||
33
sg_wechat_enterprise/we_api/__init__.py
Normal file
33
sg_wechat_enterprise/we_api/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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())
|
||||||
21
sg_wechat_enterprise/we_api/_compat.py
Normal file
21
sg_wechat_enterprise/we_api/_compat.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- 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
|
||||||
140
sg_wechat_enterprise/we_api/client/__init__.py
Normal file
140
sg_wechat_enterprise/we_api/client/__init__.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# -*- 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 会缓存起来。
|
||||||
|
# 如果外部已经缓存,这里只需要传入 appid,component和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
|
||||||
23
sg_wechat_enterprise/we_api/client/api/__init__.py
Normal file
23
sg_wechat_enterprise/we_api/client/api/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- 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
|
||||||
29
sg_wechat_enterprise/we_api/client/api/base.py
Normal file
29
sg_wechat_enterprise/we_api/client/api/base.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# -*- 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
|
||||||
431
sg_wechat_enterprise/we_api/client/api/card.py
Normal file
431
sg_wechat_enterprise/we_api/client/api/card.py
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
# -*- 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
|
||||||
|
)
|
||||||
242
sg_wechat_enterprise/we_api/client/api/customservice.py
Normal file
242
sg_wechat_enterprise/we_api/client/api/customservice.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# -*- 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
|
||||||
360
sg_wechat_enterprise/we_api/client/api/datacube.py
Normal file
360
sg_wechat_enterprise/we_api/client/api/datacube.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# -*- 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
|
||||||
284
sg_wechat_enterprise/we_api/client/api/device.py
Normal file
284
sg_wechat_enterprise/we_api/client/api/device.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# -*- 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
|
||||||
|
}
|
||||||
|
)
|
||||||
148
sg_wechat_enterprise/we_api/client/api/group.py
Normal file
148
sg_wechat_enterprise/we_api/client/api/group.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# -*- 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
58
sg_wechat_enterprise/we_api/client/api/jsapi.py
Normal file
58
sg_wechat_enterprise/we_api/client/api/jsapi.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# -*- 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
|
||||||
167
sg_wechat_enterprise/we_api/client/api/material.py
Normal file
167
sg_wechat_enterprise/we_api/client/api/material.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# -*- 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')
|
||||||
126
sg_wechat_enterprise/we_api/client/api/media.py
Normal file
126
sg_wechat_enterprise/we_api/client/api/media.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# -*- 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
|
||||||
235
sg_wechat_enterprise/we_api/client/api/menu.py
Normal file
235
sg_wechat_enterprise/we_api/client/api/menu.py
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
# -*- 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}
|
||||||
|
)
|
||||||
72
sg_wechat_enterprise/we_api/client/api/merchant/__init__.py
Normal file
72
sg_wechat_enterprise/we_api/client/api/merchant/__init__.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# -*- 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
|
||||||
|
}
|
||||||
|
)
|
||||||
30
sg_wechat_enterprise/we_api/client/api/merchant/category.py
Normal file
30
sg_wechat_enterprise/we_api/client/api/merchant/category.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- 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
Reference in New Issue
Block a user