Compare commits
116 Commits
feature/or
...
feature/we
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0082a308fa | ||
|
|
5024a9254d | ||
|
|
ae844cf203 | ||
|
|
dd5c9775fc | ||
|
|
82c274591c | ||
|
|
4f73f57ddf | ||
|
|
269141dfb2 | ||
|
|
cab0e1ce0b | ||
|
|
d4ff7ffaa9 | ||
|
|
d238d09cc3 | ||
|
|
4cee5213bb | ||
|
|
35fda7106a | ||
|
|
b8baa84270 | ||
|
|
6591e663b6 | ||
|
|
bad5c8d489 | ||
|
|
bbf62d2302 | ||
|
|
17b09c1f6d | ||
|
|
e0fc70ec60 | ||
|
|
4558fc0336 | ||
|
|
4609ec442a | ||
|
|
85fea64f49 | ||
|
|
f74215c9f6 | ||
|
|
cc8906980c | ||
|
|
0990d73075 | ||
|
|
7d877a0cbb | ||
|
|
e13bad8483 | ||
|
|
22ebb1bbe1 | ||
|
|
bc888d7984 | ||
|
|
c92d4f7868 | ||
|
|
667a2a81fb | ||
|
|
4a22071f94 | ||
|
|
ec2075c874 | ||
|
|
ebc9716019 | ||
|
|
72c9c9872e | ||
|
|
2b541fe041 | ||
|
|
f6e371f223 | ||
|
|
0aefe9e656 | ||
|
|
4f8f29e41a | ||
|
|
ffb32c7ce2 | ||
|
|
3cacca80e9 | ||
|
|
a3356fe195 | ||
|
|
3c70e1623e | ||
|
|
51628c081a | ||
|
|
18ebe6bcb4 | ||
|
|
63ac4f2b44 | ||
|
|
1b6ca170c0 | ||
|
|
2966260f51 | ||
|
|
c4069995e0 | ||
|
|
54920982a2 | ||
|
|
f947a3ed58 | ||
|
|
5e86d67316 | ||
|
|
7354a19696 | ||
|
|
78e3d77000 | ||
|
|
47ad2410cf | ||
|
|
fa2eb7b3cf | ||
|
|
73f161ffa4 | ||
|
|
ff06d5c3be | ||
|
|
78cb177ac5 | ||
|
|
9601502360 | ||
|
|
739b7e600c | ||
|
|
27da7639b2 | ||
|
|
258e3bdb9f | ||
|
|
7f3e9927d5 | ||
|
|
927f726030 | ||
|
|
7576200fc4 | ||
|
|
4ffb9d636f | ||
|
|
24f0547343 | ||
|
|
402a323673 | ||
|
|
5e1685417f | ||
|
|
9dfe34ce9a | ||
|
|
190d6da217 | ||
|
|
8946b44280 | ||
|
|
f21d085462 | ||
|
|
43f53197c4 | ||
|
|
ac43be1262 | ||
|
|
8bf1c56efd | ||
|
|
d66791dc41 | ||
|
|
ce6b36a77e | ||
|
|
4acae01009 | ||
|
|
213b76a280 | ||
|
|
c675e1d67c | ||
|
|
8a9f6ab34f | ||
|
|
8aca6ce084 | ||
|
|
cb0c093006 | ||
|
|
1e7d7008b4 | ||
|
|
ef453dbc1e | ||
|
|
9f1e635c8d | ||
|
|
92037f3f04 | ||
|
|
4ba0566d27 | ||
|
|
5d870d53f6 | ||
|
|
6a674cdb5b | ||
|
|
fbce132173 | ||
|
|
7ce2f6c797 | ||
|
|
7502792438 | ||
|
|
9efb13cf01 | ||
|
|
b14e263a2e | ||
|
|
6f41ef3047 | ||
|
|
de29f0c938 | ||
|
|
589a24f595 | ||
|
|
85186f94f5 | ||
|
|
ffc363a31c | ||
|
|
a047ad3bb6 | ||
|
|
cc3289c834 | ||
|
|
e1093a3c69 | ||
|
|
ba955ca658 | ||
|
|
f8a12b1fe3 | ||
|
|
ccbe311c58 | ||
|
|
55694ae303 | ||
|
|
5a7ba7f87e | ||
|
|
d36da3df24 | ||
|
|
dec5ddd154 | ||
|
|
01c57a8691 | ||
|
|
2808005ce9 | ||
|
|
2ddfbb0dde | ||
|
|
d5c82e4a28 | ||
|
|
436adc5ff6 |
@@ -5,9 +5,9 @@
|
||||
<!-- 修改页面头部图标及文字 -->
|
||||
<template id="favicon_icon" inherit_id="web.layout" name="Web layout">
|
||||
<!-- change the title with reliance partner -->
|
||||
<xpath expr="//head//title" position="before">
|
||||
<!-- <xpath expr="//head//title" position="before">
|
||||
<title t-esc="'JIKIMO'"/>
|
||||
</xpath>
|
||||
</xpath> -->
|
||||
<!-- change the default favicon icon with -->
|
||||
<xpath expr="//head//link[@rel='shortcut icon']" position="replace">
|
||||
<link type="image/x-icon" rel="shortcut icon" href="/jikimo_frontend/static/src/img/jikimo-logo.ico"/>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- hide 登录页面 powerd by odoo 及管理数据库 -->
|
||||
<template id="login_page_layout" inherit_id="web.login_layout" name="Login Page Layout">
|
||||
<xpath expr="//div[@class='card-body']//div[last()]" position="replace"></xpath>
|
||||
<xpath expr="//div[@class='card-body']/div[last()]" position="replace"></xpath>
|
||||
</template>
|
||||
|
||||
<!-- 隐藏odoo版本信息 -->
|
||||
|
||||
1
jikimo_sale_order_message_notify/__init__.py
Normal file
1
jikimo_sale_order_message_notify/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
26
jikimo_sale_order_message_notify/__manifest__.py
Normal file
26
jikimo_sale_order_message_notify/__manifest__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': '机企猫智能工厂 消息提醒',
|
||||
'version': '1.0',
|
||||
'summary': '智能工厂消息提醒模块',
|
||||
'sequence': 1,
|
||||
'description': """
|
||||
|
||||
""",
|
||||
'category': 'sf',
|
||||
'website': 'https://www.sf.jikimo.com',
|
||||
'depends': ['jikimo_message_notify'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/bussiness_node.xml',
|
||||
# 'views/sf_message_template_view.xml',
|
||||
],
|
||||
'test': [
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
}
|
||||
9
jikimo_sale_order_message_notify/data/bussiness_node.xml
Normal file
9
jikimo_sale_order_message_notify/data/bussiness_node.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="bussiness_1" model="jikimo.message.bussiness.node">
|
||||
<field name="name">订单确认</field>
|
||||
<field name="model">sale.order</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
2
jikimo_sale_order_message_notify/models/__init__.py
Normal file
2
jikimo_sale_order_message_notify/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import jikimo_message_template
|
||||
from . import sale_order
|
||||
@@ -0,0 +1,10 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
class JikimoMessageTemplate(models.Model):
|
||||
_inherit = "jikimo.message.template"
|
||||
|
||||
def _get_message_model(self):
|
||||
res = super(JikimoMessageTemplate, self)._get_message_model()
|
||||
res.append("sale.order")
|
||||
return res
|
||||
|
||||
12
jikimo_sale_order_message_notify/models/sale_order.py
Normal file
12
jikimo_sale_order_message_notify/models/sale_order.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_name = "sale.order"
|
||||
_description = "销售订单"
|
||||
_inherit = ["sale.order", "jikimo.message.dispatch"]
|
||||
|
||||
def create(self, vals_list):
|
||||
res = super(SaleOrder, self).create(vals_list)
|
||||
res.add_queue('订单确认')
|
||||
return res
|
||||
@@ -0,0 +1,5 @@
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,2 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_theme_data,access.theme.data,model_theme_data,,1,1,1,1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- © <2016> <top hy>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="sf_message_template_view_form" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.view.form</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="消息模板">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" class="w-100" required="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<!-- <field name="type"/>-->
|
||||
<field name="notify_model_id"/>
|
||||
<field name="content" widget="html" class="oe-bordered-editor"
|
||||
options="{'style-inline': true, 'codeview': true, 'dynamic_placeholder': true}"/>
|
||||
<field name="description"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="notification_department_id"/>
|
||||
<field name="notification_employee_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sf_message_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.view.tree</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="消息模板">
|
||||
<field name="name"/>
|
||||
<!-- <field name="type"/>-->
|
||||
<field name="content"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="notification_department_id"/>
|
||||
<field name="notification_employee_ids" widget="many2many_tags"/>
|
||||
<field name="description"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sf_message_template_search_view" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.search.view</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" string="模糊搜索"
|
||||
filter_domain="['|','|',('name','like',self),('description','like',self)]"/>
|
||||
<field name="name"/>
|
||||
<filter name="filter_active" string="已归档" domain="[('active','=',False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--定义单证类型视图动作-->
|
||||
<record id="sf_message_template_action" model="ir.actions.act_window">
|
||||
<field name="name">消息模板</field>
|
||||
<field name="res_model">message.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="sf_message_template_view_tree"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="msg_set_menu" name="消息设置" parent="base.menu_administration" sequence="1"/>
|
||||
<menuitem id="sf_message_template_send_menu" name="消息模板" parent="msg_set_menu"
|
||||
action="sf_message_template_action" sequence="1"/>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,2 +1,3 @@
|
||||
pystrich
|
||||
cpca
|
||||
pycryptodome==3.20
|
||||
|
||||
@@ -21,8 +21,9 @@ class ToolMaterialsBasicParameters(models.Model):
|
||||
neck_length = fields.Float('颈部长度(mm)')
|
||||
handle_diameter = fields.Float('柄部直径(mm)')
|
||||
handle_length = fields.Float('柄部长度(mm)')
|
||||
blade_tip_diameter = fields.Integer('刀尖直径(mm)')
|
||||
blade_tip_working_size = fields.Char('刀尖处理尺寸(R半径mm/倒角度)', size=20)
|
||||
blade_tip_diameter = fields.Float('刀尖直径(mm)')
|
||||
blade_tip_working_size = fields.Char('刀尖倒角度(°)', size=20)
|
||||
tip_r_size = fields.Float('刀尖R角(mm)')
|
||||
blade_tip_taper = fields.Integer('刀尖锥度(°)')
|
||||
blade_diameter = fields.Float('刃部直径(mm)')
|
||||
blade_length = fields.Float('刃部长度(mm)')
|
||||
|
||||
@@ -242,3 +242,8 @@ access_sf_fixture_materials_basic_parameters_group_sf_stock_manager,sf_fixture_m
|
||||
access_sf_multi_mounting_type_group_sf_stock_manager,sf_multi_mounting_type_group_sf_stock_manager,model_sf_multi_mounting_type,sf_base.group_sf_stock_manager,1,0,0,0
|
||||
access_sf_machine_brand_group_sf_stock_manager,sf_machine_brand_group_sf_stock_manager,model_sf_machine_brand,sf_base.group_sf_stock_manager,1,0,0,0
|
||||
access_sf_cutting_tool_type_group_sf_stock_manager,sf_cutting_tool_type_group_sf_stock_manager,model_sf_cutting_tool_type,sf_base.group_sf_stock_manager,1,0,0,0
|
||||
|
||||
|
||||
access_sf_cutting_tool_material_group_plan_dispatch,sf_cutting_tool_material_group_plan_dispatch,model_sf_cutting_tool_material,sf_base.group_plan_dispatch,1,0,0,0
|
||||
access_sf_functional_cutting_tool_model_group_plan_dispatch,sf_functional_cutting_tool_model_group_plan_dispatch,model_sf_functional_cutting_tool_model,sf_base.group_plan_dispatch,1,0,0,0
|
||||
access_sf_cutting_tool_type_group_plan_dispatch,sf_cutting_tool_type_group_plan_dispatch,model_sf_cutting_tool_type,sf_base.group_plan_dispatch,1,0,0,0
|
||||
|
||||
|
@@ -30,6 +30,7 @@ patch(barcodeGenericHandlers, "start", {
|
||||
"O-CMD.PAGER-FIRST": () => updatePager("first"),
|
||||
"O-CMD.PAGER-LAST": () => updatePager("last"),
|
||||
"O-CMD.CONFIRM": () => customClickOnButton(".jikimo_button_confirm"),
|
||||
"O-CMD.FLUSHED": () => customClickOnButton(".jikimo_button_flushed"),
|
||||
};
|
||||
|
||||
barcode.bus.addEventListener("barcode_scanned", (ev) => {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<field name="handle_length"/>
|
||||
<field name="blade_tip_diameter"/>
|
||||
<field name="blade_tip_working_size"/>
|
||||
<field name="tip_r_size"/>
|
||||
<field name="blade_tip_taper"/>
|
||||
<field name="blade_diameter"/>
|
||||
<field name="blade_length"/>
|
||||
@@ -95,6 +96,7 @@
|
||||
<field name="handle_length"/>
|
||||
<field name="blade_tip_diameter"/>
|
||||
<field name="blade_tip_working_size"/>
|
||||
<field name="tip_r_size"/>
|
||||
<field name="blade_tip_taper"/>
|
||||
<field name="blade_diameter"/>
|
||||
<field name="blade_length"/>
|
||||
@@ -139,6 +141,7 @@
|
||||
</group>
|
||||
<group attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')]}">
|
||||
<field name="blade_tip_working_size"/>
|
||||
<field name="tip_r_size"/>
|
||||
<field name="blade_tip_diameter" class="diameter"/>
|
||||
<field name="blade_tip_taper"/>
|
||||
<field name="blade_helix_angle"/>
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
<field name="handle_diameter" class="diameter"/>
|
||||
<field name="handle_length"/>
|
||||
<field name="blade_tip_working_size" class="du"/>
|
||||
<field name="tip_r_size"/>
|
||||
<field name="blade_tip_diameter" class="diameter"/>
|
||||
<field name="blade_tip_taper" class="du"/>
|
||||
<field name="blade_helix_angle" class="du"/>
|
||||
@@ -577,7 +578,7 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_cutting_tool_material_search">
|
||||
<record model="ir.ui.view" id="view_cutting_tool_inventory_search">
|
||||
<field name="name">sf.tool.inventory.search</field>
|
||||
<field name="model">sf.tool.inventory</field>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import requests
|
||||
@@ -53,11 +54,14 @@ class StatusChange(models.Model):
|
||||
if not ret.get('error'):
|
||||
logging.info('接口已经执行=============')
|
||||
else:
|
||||
logging.error('工厂加工同步订单状态失败 {}'.format(ret))
|
||||
raise UserError('工厂加工同步订单状态失败')
|
||||
traceback_error = traceback.format_exc()
|
||||
logging.error("bfm订单状态同步失败:%s request info %s" % traceback_error)
|
||||
logging.error('/api/get/state/get_order 请求失败{}'.format(ret))
|
||||
raise UserError('工厂加工同步订单状态到bfm失败')
|
||||
except UserError as e:
|
||||
logging.error('工厂加工同步订单状态失败 {}'.format(e))
|
||||
raise UserError('工厂加工同步订单状态失败')
|
||||
traceback_error = traceback.format_exc()
|
||||
logging.error("工厂加工同步订单状态失败:%s " % traceback_error)
|
||||
raise UserError(e)
|
||||
return res
|
||||
|
||||
def action_cancel(self):
|
||||
|
||||
@@ -191,6 +191,8 @@
|
||||
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')],'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="cutting_tool_blade_tip_working_size"
|
||||
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')],'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="cutting_tool_blade_tip_r_size"
|
||||
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')],'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="cutting_tool_blade_tip_diameter" string="刀尖直径(mm)" class="diameter"
|
||||
attrs="{'invisible': [('cutting_tool_type', '!=', '整体式刀具')],'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="cutting_tool_blade_tip_taper" string="刀尖锥度(°)"
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
'sequence': 1,
|
||||
'category': 'sf',
|
||||
'website': 'https://www.sf.jikimo.com',
|
||||
'depends': ['base', 'hr'],
|
||||
'depends': ['hr'],
|
||||
'data': [
|
||||
'views/hr_employee.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_users_view.xml',
|
||||
'data/cron_data.xml',
|
||||
],
|
||||
'demo': [
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
|
||||
from . import hr_employee
|
||||
from . import res_config_setting
|
||||
from . import res_users
|
||||
|
||||
@@ -20,9 +20,7 @@ class JkmPracticeEmployee(models.Model):
|
||||
if result['employee_list']:
|
||||
for employee_info in result['employee_list']:
|
||||
if employee_info['work_email']:
|
||||
hr_employee = self.sudo().search([('work_email', '=', employee_info['work_email'])])
|
||||
hr_employee.write({'we_id': employee_info['we_id']})
|
||||
if hr_employee.user_id:
|
||||
hr_employee.user_id.write({'we_employee_id': employee_info['we_id']})
|
||||
self.sudo().search([('work_email', '=', employee_info['work_email'])]).write(
|
||||
{'we_id': employee_info['we_id']})
|
||||
else:
|
||||
logging.info('_employee_info_sync error:%s' % result['message'])
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
we_employee_id = fields.Char(string=u'企业微信账号', default="")
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_users_account_form" model="ir.ui.view">
|
||||
<field name="name">res.users.account.form</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<page name="preferences" position="after">
|
||||
<page name="account" string="企业微信">
|
||||
<group>
|
||||
<field name="we_employee_id"/>
|
||||
</group>
|
||||
</page>
|
||||
</page>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -623,7 +623,7 @@ class Sf_Dashboard_Connect(http.Controller):
|
||||
# 未完成订单
|
||||
not_done_orders = plan_obj.search(
|
||||
[('production_line_id.name', '=', line), ('state', 'not in', ['finished']),
|
||||
('production_id.state', 'not in', ['cancel']), ('active', '=', True)
|
||||
('production_id.state', 'not in', ['cancel', 'done']), ('active', '=', True)
|
||||
])
|
||||
# print(not_done_orders)
|
||||
# 完成订单
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<h2>获取检测报告服务配置</h2>
|
||||
<div class="row mt16 o_settings_container" id="jd_api">
|
||||
<div class="row mt16 o_settings_container" id="check_report_config">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
@@ -38,6 +38,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='check_report_config']/div" position="after">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="is_get_detection_file"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<div class="text-muted">
|
||||
<label for="is_get_detection_file"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
@@ -38,8 +38,7 @@ class SfMaintenanceEquipment(models.Model):
|
||||
|
||||
crea_url = "/api/machine_tool/create"
|
||||
|
||||
|
||||
#AGV运行日志
|
||||
# AGV运行日志
|
||||
agv_logs = fields.One2many('maintenance.equipment.agv.log', 'equipment_id', string='AGV运行日志')
|
||||
# 1212修改后的字段
|
||||
number_of_axles = fields.Selection(
|
||||
@@ -117,7 +116,6 @@ class SfMaintenanceEquipment(models.Model):
|
||||
# num = "%04d" % m
|
||||
# return num
|
||||
|
||||
|
||||
equipment_maintenance_standards_ids = fields.Many2many('equipment.maintenance.standards',
|
||||
'sf_maintenance_equipment_ids', string='设备维保标准')
|
||||
eq_maintenance_id = fields.Many2one('equipment.maintenance.standards', string='设备保养标准',
|
||||
@@ -179,7 +177,8 @@ class SfMaintenanceEquipment(models.Model):
|
||||
type_id = fields.Many2one('sf.machine_tool.type', '型号')
|
||||
|
||||
state = fields.Selection(
|
||||
[("正常", "正常"), ("故障停机", "故障停机"), ("计划维保", "计划维保"), ("空闲", "空闲"), ("封存(报废)", "封存(报废)")],
|
||||
[("正常", "正常"), ("故障停机", "故障停机"), ("计划维保", "计划维保"), ("空闲", "空闲"),
|
||||
("封存(报废)", "封存(报废)")],
|
||||
default='正常', string="机床状态")
|
||||
run_time = fields.Char('总运行时长')
|
||||
# 0606新增字段
|
||||
@@ -328,7 +327,7 @@ class SfMaintenanceEquipment(models.Model):
|
||||
item.tool_diameter_min = item.type_id.tool_diameter_min
|
||||
item.machine_tool_category = item.type_id.machine_tool_category.id
|
||||
item.brand_id = item.type_id.brand_id.id
|
||||
#新增修改字段
|
||||
# 新增修改字段
|
||||
item.taper_type_id = item.type_id.taper_type_id.id
|
||||
item.function_type = item.type_id.function_type
|
||||
item.a_axis = item.type_id.a_axis
|
||||
@@ -370,7 +369,6 @@ class SfMaintenanceEquipment(models.Model):
|
||||
item.image_id = item.type_id.jg_image_id.ids
|
||||
item.image_lq_id = item.type_id.lq_image_id.ids
|
||||
|
||||
|
||||
# AGV小车设备参数
|
||||
AGV_L = fields.Char('AGV尺寸(长)')
|
||||
AGV_W = fields.Char('AGV尺寸(宽)')
|
||||
@@ -461,18 +459,6 @@ class SfMaintenanceEquipment(models.Model):
|
||||
original_value = fields.Char('原值')
|
||||
incomplete_value = fields.Char('残值')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 注册同步机床
|
||||
def enroll_machine_tool(self):
|
||||
sf_sync_config = self.env['res.config.settings'].get_values()
|
||||
@@ -763,7 +749,7 @@ class SfMaintenanceEquipment(models.Model):
|
||||
image_id = fields.Many2many('maintenance.equipment.image', 'equipment_id',
|
||||
domain="[('type', '=', '加工能力')]")
|
||||
image_lq_id = fields.Many2many('maintenance.equipment.image', 'equipment_lq_id', string='冷却方式',
|
||||
domain="[('type', '=', '冷却方式')]")
|
||||
domain="[('type', '=', '冷却方式')]")
|
||||
|
||||
|
||||
class SfRobotAxisNum(models.Model):
|
||||
@@ -777,4 +763,5 @@ class SfRobotAxisNum(models.Model):
|
||||
weight = fields.Char('最大负载(kg)')
|
||||
permissible_load_torque = fields.Char('允许负载扭矩(N-m)')
|
||||
permissible_inertial_torque = fields.Char('允许惯性扭矩(kg-m²)')
|
||||
equipment_id = fields.Many2one('maintenance.equipment', string='机器人', domain="[('equipment_type', '=', '机器人')]")
|
||||
equipment_id = fields.Many2one('maintenance.equipment', string='机器人',
|
||||
domain="[('equipment_type', '=', '机器人')]")
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
'sf_manufacturing/static/src/scss/kanban_change.scss',
|
||||
'sf_manufacturing/static/src/xml/button_show_on_tree.xml',
|
||||
'sf_manufacturing/static/src/js/workpiece_delivery_wizard_confirm.js',
|
||||
'sf_manufacturing/static/src/js/custom_barcode_handlers.js',
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class AgvScheduling(models.Model):
|
||||
def web_search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, count_limit=None):
|
||||
domain = domain or []
|
||||
new_domain = []
|
||||
for index, item in enumerate(domain):
|
||||
for item in domain:
|
||||
if isinstance(item, list):
|
||||
if item[0] == 'delivery_workpieces':
|
||||
new_domain.append('&')
|
||||
@@ -63,7 +63,7 @@ class AgvScheduling(models.Model):
|
||||
continue
|
||||
new_domain.append(item)
|
||||
|
||||
return super(AgvScheduling, self).web_search_read(new_domain, fields, limit=limit, offset=offset)
|
||||
return super(AgvScheduling, self).web_search_read(new_domain, fields, offset, limit, order, count_limit)
|
||||
|
||||
@api.depends('task_completion_time', 'task_delivery_time')
|
||||
def _compute_task_duration(self):
|
||||
|
||||
@@ -318,8 +318,10 @@ class MrpProduction(models.Model):
|
||||
# cnc程序获取
|
||||
def fetchCNC(self, production_names):
|
||||
cnc = self.env['mrp.production'].search([('id', '=', self.id)])
|
||||
quick_order = self.env['quick.easy.order'].search(
|
||||
[('name', '=', cnc.product_id.default_code.rsplit('-', 1)[0])])
|
||||
quick_order = False
|
||||
if cnc.product_id.default_code:
|
||||
quick_order = self.env['quick.easy.order'].search(
|
||||
[('name', '=', cnc.product_id.default_code.rsplit('-', 1)[0])])
|
||||
programme_way = False
|
||||
if cnc.manual_quotation is True:
|
||||
programme_way = 'manual operation'
|
||||
@@ -804,6 +806,10 @@ class MrpProduction(models.Model):
|
||||
backorders = backorders - productions_to_backorder
|
||||
|
||||
productions_not_to_backorder._post_inventory(cancel_backorder=True)
|
||||
# if self.workorder_ids.filtered(lambda w: w.routing_type in ['表面工艺']):
|
||||
# move_finish = self.env['stock.move'].search([('created_production_id', '=', self.id)])
|
||||
# if move_finish:
|
||||
# move_finish._action_assign()
|
||||
productions_to_backorder._post_inventory(cancel_backorder=True)
|
||||
|
||||
# if completed products make other confirmed/partially_available moves available, assign them
|
||||
|
||||
@@ -144,6 +144,8 @@ class ResMrpWorkOrder(models.Model):
|
||||
# 是否绑定托盘
|
||||
is_trayed = fields.Boolean(string='是否绑定托盘', default=False)
|
||||
|
||||
tag_type = fields.Selection([("重新加工", "重新加工")], string="标签", tracking=True)
|
||||
|
||||
@api.depends('name', 'production_id.name')
|
||||
def _compute_surface_technics_picking_ids(self):
|
||||
for workorder in self:
|
||||
@@ -426,17 +428,18 @@ class ResMrpWorkOrder(models.Model):
|
||||
logging.info('local_file_path:%s' % local_file_path)
|
||||
remote_path = '/home/ftp/ftp_root/ThreeTest/XT/Before/' + local_filename
|
||||
logging.info('remote_path:%s' % remote_path)
|
||||
# if not ftp.file_exists(remote_path):
|
||||
paload_data = {
|
||||
"filename": local_filename
|
||||
}
|
||||
if not ftp_resconfig['get_check_file_path']:
|
||||
raise UserError('请先配置获取检测报告地址')
|
||||
url = ftp_resconfig['get_check_file_path'] + '/get/check/report'
|
||||
response = requests.post(url, json=paload_data)
|
||||
logging.info('response:%s' % response.json())
|
||||
if response.json().get('detail'):
|
||||
raise UserError(response.json().get('detail'))
|
||||
is_get_detection_file = self.env['ir.config_parameter'].sudo().get_param('is_get_detection_file')
|
||||
if not is_get_detection_file:
|
||||
paload_data = {
|
||||
"filename": local_filename
|
||||
}
|
||||
if not ftp_resconfig['get_check_file_path']:
|
||||
raise UserError('请先配置获取检测报告地址')
|
||||
url = ftp_resconfig['get_check_file_path'] + '/get/check/report'
|
||||
response = requests.post(url, json=paload_data)
|
||||
logging.info('response:%s' % response.json())
|
||||
if response.json().get('detail'):
|
||||
raise UserError(response.json().get('detail'))
|
||||
|
||||
if not ftp.file_exists(remote_path):
|
||||
raise UserError(f"文件不存在: {remote_path}")
|
||||
@@ -603,6 +606,8 @@ class ResMrpWorkOrder(models.Model):
|
||||
print("(%.2f,%.2f)" % (x, y))
|
||||
self.material_center_point = ("(%.2f,%.2f,%.2f)" % (x, y, z))
|
||||
self.X_deviation_angle = jdz
|
||||
logging.info("坯料中心点坐标:(%.2f,%.2f)" % (x, y))
|
||||
logging.info("X轴偏差度数:%.2f" % jdz)
|
||||
# 将补偿值写入CNC加工工单
|
||||
workorder = self.env['mrp.workorder'].browse(self.ids)
|
||||
work = workorder.production_id.workorder_ids
|
||||
@@ -705,6 +710,7 @@ class ResMrpWorkOrder(models.Model):
|
||||
'date_planned_finished': datetime.now() + timedelta(days=1),
|
||||
'duration_expected': duration_expected,
|
||||
'duration': 0,
|
||||
'tag_type': '重新加工' if item is False else False,
|
||||
'cnc_ids': False if route.routing_type != 'CNC加工' else self.env['sf.cnc.processing']._json_cnc_processing(
|
||||
k, item),
|
||||
'cmm_ids': False if route.routing_type != 'CNC加工' else self.env['sf.cmm.program']._json_cmm_program(k,
|
||||
@@ -1025,16 +1031,20 @@ class ResMrpWorkOrder(models.Model):
|
||||
# 查询工序最小的非完工、非返工的装夹预调工单
|
||||
work_id = self.search(
|
||||
[('production_id', '=', workorder.production_id.id),
|
||||
('routing_type', '=', '装夹预调'),
|
||||
('state', 'not in', ['rework', 'done', 'cancel'])],
|
||||
limit=1,
|
||||
order="sequence")
|
||||
if workorder == work_id:
|
||||
if workorder.production_id.reservation_state == 'assigned':
|
||||
workorder.state = 'ready'
|
||||
elif workorder.production_id.reservation_state != 'assigned':
|
||||
workorder.state = 'waiting'
|
||||
continue
|
||||
if work_id.routing_type == '装夹预调':
|
||||
if workorder == work_id:
|
||||
if workorder.production_id.reservation_state == 'assigned':
|
||||
workorder.state = 'ready'
|
||||
elif workorder.production_id.reservation_state != 'assigned':
|
||||
workorder.state = 'waiting'
|
||||
continue
|
||||
elif (workorder.name == '装夹预调' and
|
||||
workorder.state not in ['rework', 'done', 'cancel']):
|
||||
if workorder.state != 'pending':
|
||||
workorder.state = 'pending'
|
||||
if workorder.production_id.tool_state in ['1', '2'] and workorder.state == 'ready':
|
||||
workorder.state = 'waiting'
|
||||
continue
|
||||
@@ -1179,8 +1189,10 @@ class ResMrpWorkOrder(models.Model):
|
||||
if not record.rfid_code and record.is_rework is False:
|
||||
raise UserError("请扫RFID码进行绑定")
|
||||
if record.is_rework is False:
|
||||
if not record.material_center_point or record.X_deviation_angle <= 0:
|
||||
raise UserError("坯料中心点为空或X偏差角度小于等于0")
|
||||
if not record.material_center_point:
|
||||
raise UserError("坯料中心点为空,请检查")
|
||||
if record.X_deviation_angle <= 0:
|
||||
raise UserError("X偏差角度小于等于0,请检查!本次计算的X偏差角度为:%s" % record.X_deviation_angle)
|
||||
record.process_state = '待加工'
|
||||
# record.write({'process_state': '待加工'})
|
||||
record.production_id.process_state = '待加工'
|
||||
@@ -1338,6 +1350,7 @@ class ResMrpWorkOrder(models.Model):
|
||||
arch = etree.fromstring(tree_view['arch'])
|
||||
# 查找 tree 标签
|
||||
tree_element = arch.xpath("//tree")[0]
|
||||
tree_element.set('js_class', 'remove_focus_list_view')
|
||||
|
||||
# 查找或创建 header 标签
|
||||
header_element = tree_element.find('header')
|
||||
@@ -1560,6 +1573,8 @@ class SfWorkOrderBarcodes(models.Model):
|
||||
|
||||
def on_barcode_scanned(self, barcode):
|
||||
logging.info('Rfid:%s' % barcode)
|
||||
if 'O-CMD' in barcode:
|
||||
return None
|
||||
workorder = self.env['mrp.workorder'].browse(self.ids)
|
||||
# workorder_preset = self.env['mrp.workorder'].search(
|
||||
# [('routing_type', '=', '装夹预调'), ('rfid_code', '=', barcode)])
|
||||
@@ -1567,8 +1582,11 @@ class SfWorkOrderBarcodes(models.Model):
|
||||
[('routing_type', '=', '装夹预调'), ('rfid_code', '=', barcode)])
|
||||
if workorder_olds:
|
||||
name = ''
|
||||
tem_list = []
|
||||
for workorder in workorder_olds:
|
||||
name = '%s %s' % (name, workorder.production_id.name)
|
||||
tem_list.append(workorder.production_id.name)
|
||||
for i in list(set(tem_list)):
|
||||
name = '%s %s' % (name, i)
|
||||
raise UserError('该托盘已绑定【%s】制造订单,请先解除绑定!!!' % name)
|
||||
if workorder:
|
||||
if workorder.routing_type == '装夹预调':
|
||||
|
||||
@@ -90,7 +90,8 @@ class ResProductMo(models.Model):
|
||||
cutting_tool_coarse_medium_fine = fields.Selection([('粗', '粗'), ('中', '中'), ('精', '精')], '粗/中/精')
|
||||
cutting_tool_run_out_accuracy_max = fields.Float('端跳精度max', digits=(6, 1))
|
||||
cutting_tool_run_out_accuracy_min = fields.Float('端跳精度min', digits=(6, 1))
|
||||
cutting_tool_blade_tip_working_size = fields.Char('刀尖处理尺寸(R半径mm/倒角)', size=20)
|
||||
cutting_tool_blade_tip_working_size = fields.Char('刀尖倒角度(°)', size=20)
|
||||
cutting_tool_blade_tip_r_size = fields.Float('刀尖R角(mm)')
|
||||
fit_blade_shape_id = fields.Many2one('maintenance.equipment.image',
|
||||
'适配刀片形状', domain=[('type', '=', '刀片形状')])
|
||||
suitable_machining_method_ids = fields.Many2many('maintenance.equipment.image',
|
||||
@@ -237,6 +238,7 @@ class ResProductMo(models.Model):
|
||||
self.cutting_tool_blade_tip_taper = self.specification_id.blade_tip_taper
|
||||
self.cutting_tool_blade_helix_angle = self.specification_id.blade_helix_angle
|
||||
self.cutting_tool_blade_tip_working_size = self.specification_id.blade_tip_working_size
|
||||
self.cutting_tool_blade_tip_r_size = self.specification_id.tip_r_size
|
||||
self.cutting_tool_pitch = self.specification_id.pitch
|
||||
self.cutting_tool_blade_width = self.specification_id.blade_width
|
||||
self.cutting_tool_blade_depth = self.specification_id.blade_depth
|
||||
|
||||
@@ -4,19 +4,39 @@ from odoo import models, fields, api
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
agv_rcs_url = fields.Char(string='avg_rcs访问地址',
|
||||
default='http://172.16.10.114:8182/rcms/services/rest/hikRpcService/genAgvSchedulingTask')
|
||||
wbcode = fields.Char('地码')
|
||||
agv_code = fields.Char(string='agv编号')
|
||||
task_type_no = fields.Char('任务单类型编号')
|
||||
|
||||
is_agv_task_dispatch = fields.Boolean('是否下发AGV任务', default=False)
|
||||
# 是否重新获取检测文件
|
||||
is_get_detection_file = fields.Boolean(string='重新获取检测文件', default=False)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
values = super(ResConfigSettings, self).get_values()
|
||||
config = self.env['ir.config_parameter'].sudo()
|
||||
agv_rcs_url = config.get_param('agv_rcs_url', default='')
|
||||
wbcode = config.get_param('wbcode', default='')
|
||||
agv_code = config.get_param('agv_code', default='')
|
||||
is_agv_task_dispatch = config.get_param('is_agv_task_dispatch')
|
||||
is_get_detection_file = config.get_param('is_get_detection_file')
|
||||
values.update(
|
||||
agv_rcs_url=agv_rcs_url,
|
||||
wbcode=wbcode,
|
||||
agv_code=agv_code,
|
||||
is_agv_task_dispatch=is_agv_task_dispatch,
|
||||
is_get_detection_file=is_get_detection_file
|
||||
)
|
||||
return values
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
config = self.env['ir.config_parameter'].sudo()
|
||||
config.set_param("agv_rcs_url", self.agv_rcs_url or "")
|
||||
config.set_param("wbcode", self.wbcode or "")
|
||||
config.set_param("agv_code", self.agv_code or "")
|
||||
config.set_param("is_agv_task_dispatch", self.is_agv_task_dispatch or False)
|
||||
config.set_param("is_get_detection_file", self.is_get_detection_file or False)
|
||||
|
||||
@@ -269,8 +269,9 @@ class StockRule(models.Model):
|
||||
sale_order = self.env['sale.order'].sudo().search([('name', '=', production.origin)])
|
||||
# 根据销售订单号查询快速订单
|
||||
quick_easy_order = self.env['quick.easy.order'].sudo().search([('sale_order_id', '=', sale_order.id)])
|
||||
production.write({'part_number': quick_easy_order.part_drawing_number,
|
||||
'part_drawing': quick_easy_order.machining_drawings})
|
||||
if quick_easy_order:
|
||||
production.write({'part_number': quick_easy_order.part_drawing_number,
|
||||
'part_drawing': quick_easy_order.machining_drawings})
|
||||
if sale_order:
|
||||
# sale_order.write({'schedule_status': 'to schedule'})
|
||||
self.env['sf.production.plan'].sudo().with_company(company_id).create({
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
</field>
|
||||
<xpath expr="//field[@name='qty_remaining']" position="after">
|
||||
<field name="manual_quotation" optional="show"/>
|
||||
<field name='tag_type' widget="badge"
|
||||
decoration-danger="tag_type == '重新加工'"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='date_planned_start']" position="replace">
|
||||
<field name="date_planned_start" string="计划开始日期" optional="show"/>
|
||||
@@ -43,11 +45,11 @@
|
||||
<field name="date_planned_finished" string="计划结束日期" optional="hide"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='button_start']" position="attributes">
|
||||
<!-- <attribute name="attrs">{'invisible': ['|', '|', '|','|','|', ('production_state','in', ('draft',-->
|
||||
<!-- 'done',-->
|
||||
<!-- 'cancel')), ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'cancel')),-->
|
||||
<!-- ('is_user_working', '!=', False),("user_permissions","=",False),("name","=","CNC加工")]}-->
|
||||
<!-- </attribute>-->
|
||||
<!-- <attribute name="attrs">{'invisible': ['|', '|', '|','|','|', ('production_state','in', ('draft',-->
|
||||
<!-- 'done',-->
|
||||
<!-- 'cancel')), ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'cancel')),-->
|
||||
<!-- ('is_user_working', '!=', False),("user_permissions","=",False),("name","=","CNC加工")]}-->
|
||||
<!-- </attribute>-->
|
||||
<attribute name="attrs">{'invisible': ['|', '|', '|','|','|', ('production_state','in', ('draft',
|
||||
'done',
|
||||
'cancel')), ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'cancel')),
|
||||
@@ -165,8 +167,8 @@
|
||||
<!-- attrs="{'invisible': ['|', '|', ('production_state', 'not in', ('pending_processing', 'pending_cam', 'pending_era_cam')), ('state','!=','progress'), ('routing_type', 'not in', ('装夹预调', 'CNC加工', '解除装夹'))]}" -->
|
||||
<!-- groups="sf_base.group_sf_mrp_user" confirm="是否确认完工"/> -->
|
||||
|
||||
<!-- <button name="button_start" type="object" string="开始" class="btn-success" confirm="是否确认开始"-->
|
||||
<!-- attrs="{'invisible': ['|', '|', '|', ('production_state','in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'cancel','to be detected')), ('is_user_working', '!=', False)]}"/>-->
|
||||
<!-- <button name="button_start" type="object" string="开始" class="btn-success" confirm="是否确认开始"-->
|
||||
<!-- attrs="{'invisible': ['|', '|', '|', ('production_state','in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'cancel','to be detected')), ('is_user_working', '!=', False)]}"/>-->
|
||||
<button name="button_start" type="object" string="开始" class="btn-success" confirm="是否确认开始"
|
||||
attrs="{'invisible': ['|', '|', '|', '|', '|', ('routing_type', '=', '装夹预调'), ('routing_type', '=', '解除装夹'), ('production_state','in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'cancel','to be detected')), ('is_user_working', '!=', False)]}"/>
|
||||
<button name="button_start" type="object" string="开始" class="btn-success"
|
||||
@@ -191,8 +193,8 @@
|
||||
<!-- context="{'default_workcenter_id': workcenter_id}" class="btn-danger" -->
|
||||
<!-- groups="sf_base.group_sf_mrp_user" -->
|
||||
<!-- attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done', 'cancel')), ('working_state', '!=', 'blocked'),('state','=','done')]}"/> -->
|
||||
<!-- <button name="button_workpiece_delivery" type="object" string="工件配送" class="btn-primary"-->
|
||||
<!-- attrs="{'invisible': ['|','|','|','|',('routing_type','!=','装夹预调'),('is_delivery','=',True),('state','!=','done'),('is_rework','=',True),'&',('rfid_code','in',['',False]),('state','=','done')]}"/>-->
|
||||
<!-- <button name="button_workpiece_delivery" type="object" string="工件配送" class="btn-primary"-->
|
||||
<!-- attrs="{'invisible': ['|','|','|','|',('routing_type','!=','装夹预调'),('is_delivery','=',True),('state','!=','done'),('is_rework','=',True),'&',('rfid_code','in',['',False]),('state','=','done')]}"/>-->
|
||||
<button name="button_rework_pre" type="object" string="返工"
|
||||
class="btn-primary"
|
||||
attrs="{'invisible': ['|','|',('routing_type','!=','装夹预调'),('state','!=','progress'),('is_rework','=',True)]}"/>
|
||||
@@ -221,9 +223,12 @@
|
||||
<xpath expr="//label[1]" position="before">
|
||||
<field name='routing_type' readonly="1"/>
|
||||
<field name='process_state' attrs='{"invisible": [("routing_type","!=","装夹预调")]}'/>
|
||||
<field name='tag_type' readonly="1" attrs='{"invisible": [("tag_type","=",False)]}'
|
||||
decoration-danger="tag_type == '重新加工'"/>
|
||||
<field name="rfid_code" force_save="1" readonly="1" cache="True"
|
||||
attrs="{'invisible': [('rfid_code_old', '!=', False)]}"/>
|
||||
<field name="rfid_code_old" readonly="1" attrs="{'invisible': [('rfid_code_old', '=', False)]}"/>
|
||||
|
||||
</xpath>
|
||||
<xpath expr="//label[1]" position="attributes">
|
||||
<attribute name="string">计划加工时间</attribute>
|
||||
@@ -479,10 +484,10 @@
|
||||
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<field name="data_state" invisible="1"/>
|
||||
<button type="object" class="oe_highlight" name="get_three_check_datas" string="获取数据"
|
||||
attrs='{"invisible": ["|", "|", "|", ("material_center_point","!=",False),("state","!=","progress"),("user_permissions","=",False), ("data_state", "=", True)]}'/>
|
||||
<button type="object" class="oe_highlight" name="getcenter" string="计算定位"
|
||||
attrs='{"invisible": ["|","|", "|",("material_center_point","!=",False),("state","!=","progress"),("user_permissions","=",False), ("data_state", "=", False)]}'/>
|
||||
<!-- <button type="object" class="oe_highlight" name="get_three_check_datas" string="获取数据" -->
|
||||
<!-- attrs='{"invisible": ["|", "|", "|", ("material_center_point","!=",False),("state","!=","progress"),("user_permissions","=",False), ("data_state", "=", True)]}'/> -->
|
||||
<!-- <button type="object" class="oe_highlight" name="getcenter" string="计算定位" -->
|
||||
<!-- attrs='{"invisible": ["|","|", "|",("material_center_point","!=",False),("state","!=","progress"),("user_permissions","=",False), ("data_state", "=", False)]}'/> -->
|
||||
</div>
|
||||
|
||||
<group>
|
||||
@@ -514,8 +519,8 @@
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//form//header" position="inside">
|
||||
<button type="object" class="oe_highlight" name="get_three_check_datas" string="获取数据"
|
||||
attrs='{"invisible": [("state","!=","progress")]}'/>
|
||||
<button type="object" class="oe_highlight jikimo_button_confirm" name="get_three_check_datas"
|
||||
string="获取数据" attrs='{"invisible": [("state","!=","progress"), ("routing_type","!=","装夹预调")]}'/>
|
||||
</xpath>
|
||||
|
||||
|
||||
@@ -666,7 +671,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="工件配送" class="center" create="0" delete="0" js_class="remove_focus_list_view">
|
||||
<header>
|
||||
<button name="button_delivery" type="object" string="工件配送" class="btn-primary jikimo_button_confirm" attrs="{'force_show':1}"/>
|
||||
<button name="button_delivery" type="object" string="工件配送"
|
||||
class="btn-primary jikimo_button_confirm" attrs="{'force_show':1}"/>
|
||||
</header>
|
||||
<field name="status" widget="badge"
|
||||
decoration-success="status == '已配送'"
|
||||
@@ -678,15 +684,15 @@
|
||||
<field name="production_id"/>
|
||||
<field name="type" readonly="1"/>
|
||||
<field name="production_line_id" options="{'no_create': True}" readonly="1"/>
|
||||
<!-- <field name="route_id" options="{'no_create': True}"-->
|
||||
<!-- domain="[('route_type','in',['上产线','下产线'])]"/>-->
|
||||
<!-- <field name="route_id" options="{'no_create': True}"-->
|
||||
<!-- domain="[('route_type','in',['上产线','下产线'])]"/>-->
|
||||
<field name="feeder_station_start_id" readonly="1" force_save="1"/>
|
||||
<!-- <field name="feeder_station_destination_id" readonly="1" force_save="1"/>-->
|
||||
<!-- <field name="feeder_station_destination_id" readonly="1" force_save="1"/>-->
|
||||
<field name="is_cnc_program_down" readonly="1"/>
|
||||
<!-- <field name="rfid_code"/>-->
|
||||
<!-- <field name="task_delivery_time" readonly="1"/>-->
|
||||
<!-- <field name="task_completion_time" readonly="1"/>-->
|
||||
<!-- <field name="delivery_duration" widget="float_time"/>-->
|
||||
<!-- <field name="task_delivery_time" readonly="1"/>-->
|
||||
<!-- <field name="task_completion_time" readonly="1"/>-->
|
||||
<!-- <field name="delivery_duration" widget="float_time"/>-->
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -853,10 +859,10 @@
|
||||
<field name="domain">[('type','in',['运送空料架']),('name','not ilike','WDO')]</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="mrp.menu_mrp_manufacturing"
|
||||
name="Operations"
|
||||
sequence="10"
|
||||
parent="mrp.menu_mrp_root"
|
||||
groups="sf_base.group_sf_order_user,sf_base.group_sf_mrp_manager,sf_base.group_sf_equipment_user"/>
|
||||
<menuitem id="mrp.menu_mrp_manufacturing"
|
||||
name="Operations"
|
||||
sequence="10"
|
||||
parent="mrp.menu_mrp_root"
|
||||
groups="sf_base.group_sf_order_user,sf_base.group_sf_mrp_manager,sf_base.group_sf_equipment_user"/>
|
||||
</odoo>
|
||||
|
||||
|
||||
@@ -6,6 +6,32 @@
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[hasclass('app_settings_block')]/div" position="before">
|
||||
<div>
|
||||
<h2>AGV参数配置</h2>
|
||||
<div class="row mt16 o_settings_container" id="agv_config">
|
||||
<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="agv_rcs_url" string="访问地址"/>
|
||||
<field name="agv_rcs_url"/>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
<label for="agv_code" string="车辆编号"/>
|
||||
<field name="agv_code"/>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
<label for="wbcode"/>
|
||||
<field name="wbcode"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
|
||||
<xpath expr="//div[@id='agv_config']/div" position="after">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<group col="1">
|
||||
<field name="production_ids" readonly="1" widget="many2many_tags" string="制造订单号"/>
|
||||
<field name="delivery_type" readonly="1"/>
|
||||
<field name="feeder_station_start_id" options="{'no_create': True}" required="1"/>
|
||||
<field name="feeder_station_start_id" string="当前接驳站" options="{'no_create': True}" required="1"/>
|
||||
<field name="workcenter_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<footer>
|
||||
|
||||
@@ -179,18 +179,15 @@ class WorkpieceDeliveryWizard(models.TransientModel):
|
||||
self.feeder_station_destination_id = self.route_id.end_site_id.id
|
||||
|
||||
def on_barcode_scanned(self, barcode):
|
||||
delivery_type = self.env.context.get('default_delivery_type')
|
||||
# 判断barcode是否是数字
|
||||
if not barcode.isdigit():
|
||||
# 判断是否是AGV接驳站名称
|
||||
agv_site = self.env['sf.agv.site'].search([('name', '=', barcode)])
|
||||
if agv_site:
|
||||
if not self.feeder_station_start_id:
|
||||
self.feeder_station_start_id = agv_site.id
|
||||
else:
|
||||
if self.feeder_station_start_id.id != agv_site.id:
|
||||
raise UserError('起点接驳站不匹配!')
|
||||
self.feeder_station_start_id = agv_site.id # 修正:移除 .id
|
||||
return
|
||||
delivery_type = self.env.context.get('default_delivery_type')
|
||||
|
||||
if delivery_type == '上产线':
|
||||
workorder = self.env['mrp.workorder'].search(
|
||||
[('production_line_state', '=', '待上产线'), ('rfid_code', '=', barcode),
|
||||
@@ -210,11 +207,14 @@ class WorkpieceDeliveryWizard(models.TransientModel):
|
||||
workorder.production_line_id.id != self.production_ids[0].production_line_id.id):
|
||||
raise UserError(f'该rfid对应的制造订单号为{workorder.production_id.name}的目的生产线不一致')
|
||||
|
||||
# 调用打印成品条码方法
|
||||
workorder.print_method()
|
||||
|
||||
# 将对象添加到对应的同模型且是多对多类型里
|
||||
self.production_ids |= workorder.production_id
|
||||
self.workorder_ids |= workorder
|
||||
|
||||
down_product_agv_scheduling = self.get_down_product_agv_scheduling()
|
||||
down_product_agv_scheduling = workorder.get_down_product_agv_scheduling()
|
||||
if down_product_agv_scheduling:
|
||||
if not self.feeder_station_start_id:
|
||||
self.feeder_station_start_id = down_product_agv_scheduling.end_site_id.id
|
||||
@@ -226,4 +226,11 @@ class WorkpieceDeliveryWizard(models.TransientModel):
|
||||
raise UserError('该rfid码对应的工单不存在')
|
||||
return
|
||||
|
||||
@api.onchange('feeder_station_start_id')
|
||||
def on_start_id_change(self):
|
||||
if self.delivery_type == '运送空料架' and len(self.workorder_ids) > 0:
|
||||
down_product_agv_scheduling = self.workorder_ids[0].get_down_product_agv_scheduling()
|
||||
if down_product_agv_scheduling and self.feeder_station_start_id \
|
||||
and down_product_agv_scheduling.end_site_id.id != self.feeder_station_start_id.id:
|
||||
raise UserError('当前接驳站不匹配!')
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@
|
||||
""",
|
||||
'category': 'sf',
|
||||
'website': 'https://www.sf.jikimo.com',
|
||||
'depends': ['base', 'sf_base'],
|
||||
'depends': ['sale', 'purchase', 'sf_plan', 'jikimo_message_notify'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/sf_message_template_view.xml',
|
||||
'data/bussiness_node.xml'
|
||||
],
|
||||
'test': [
|
||||
],
|
||||
|
||||
14
sf_message/data/bussiness_node.xml
Normal file
14
sf_message/data/bussiness_node.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="bussiness_pending_order" model="jikimo.message.bussiness.node">
|
||||
<field name="name">待接单</field>
|
||||
<field name="model">sale.order</field>
|
||||
</record>
|
||||
|
||||
<record id="bussiness_to_be_confirm" model="jikimo.message.bussiness.node">
|
||||
<field name="name">确认接单</field>
|
||||
<field name="model">sale.order</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1 +1,8 @@
|
||||
from . import sf_message_template
|
||||
from . import sf_message_sale
|
||||
from . import sf_message_plan
|
||||
from . import sf_message_stock_picking
|
||||
from . import sf_message_cam_program
|
||||
from . import sf_message_functional_tool_assembly
|
||||
from . import sf_message_purchase
|
||||
from . import sf_message_workorder
|
||||
|
||||
6
sf_message/models/sf_message_cam_program.py
Normal file
6
sf_message/models/sf_message_cam_program.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessageCamProgram(models.Model):
|
||||
_name = 'sf.cam.work.order.program.knife.plan'
|
||||
_inherit = ['sf.cam.work.order.program.knife.plan', 'jikimo.message.dispatch']
|
||||
6
sf_message/models/sf_message_functional_tool_assembly.py
Normal file
6
sf_message/models/sf_message_functional_tool_assembly.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessagefunctionalToolAssembly(models.Model):
|
||||
_name = 'sf.functional.tool.assembly'
|
||||
_inherit = ['sf.functional.tool.assembly', 'jikimo.message.dispatch']
|
||||
25
sf_message/models/sf_message_plan.py
Normal file
25
sf_message/models/sf_message_plan.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessagePlan(models.Model):
|
||||
_name = 'sf.production.plan'
|
||||
_inherit = ['sf.production.plan', 'jikimo.message.dispatch']
|
||||
|
||||
# def create(self, vals_list):
|
||||
# res = super(SFMessagePlan, self).create(vals_list)
|
||||
# if res:
|
||||
# try:
|
||||
# res.add_queue('待排程')
|
||||
# except Exception as e:
|
||||
# logging.info('add_queue error:%s' % e)
|
||||
# return res
|
||||
#
|
||||
# def _get_message(self):
|
||||
# res = super(SFMessagePlan, self)._get_message()
|
||||
# if res:
|
||||
# try:
|
||||
# res.add_queue('待排程')
|
||||
# except Exception as e:
|
||||
# logging.info('_get_message error:%s' % e)
|
||||
# return res
|
||||
6
sf_message/models/sf_message_purchase.py
Normal file
6
sf_message/models/sf_message_purchase.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessagePurchase(models.Model):
|
||||
_name = 'purchase.order'
|
||||
_inherit = ['purchase.order', 'jikimo.message.dispatch']
|
||||
39
sf_message/models/sf_message_sale.py
Normal file
39
sf_message/models/sf_message_sale.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessageSale(models.Model):
|
||||
_name = 'sale.order'
|
||||
_inherit = ['sale.order', 'jikimo.message.dispatch']
|
||||
|
||||
def create(self, vals_list):
|
||||
res = super(SFMessageSale, self).create(vals_list)
|
||||
if res:
|
||||
try:
|
||||
res.add_queue('待接单')
|
||||
except Exception as e:
|
||||
logging.info('add_queue error:%s' % e)
|
||||
return res
|
||||
|
||||
# 确认接单
|
||||
def action_confirm(self):
|
||||
res = super(SFMessageSale, self).action_confirm()
|
||||
if res is True:
|
||||
try:
|
||||
self.add_queue('确认接单')
|
||||
except Exception as e:
|
||||
logging.info('add_queue error:%s' % e)
|
||||
return res
|
||||
|
||||
# 继承并重写jikimo.message.dispatch的_get_message()
|
||||
def _get_message(self, message_queue_ids):
|
||||
res = super(SFMessageSale, self)._get_message(message_queue_ids)
|
||||
if message_queue_ids.message_template_id.bussiness_node_id.name == '确认接单':
|
||||
# sale_order = self.env['sale.order'].search([('id', '=', message_queue_ids.model.res_id)])
|
||||
sale_order_line = self.env['sale.order.line'].search([('order_id', '=', int(message_queue_ids.res_id))])
|
||||
if len(sale_order_line) == 1:
|
||||
product = sale_order_line[0].product_id.name
|
||||
elif len(sale_order_line) > 1:
|
||||
product = '%s...' % sale_order_line[0].product_id.name
|
||||
res[0] = res[0].replace('{{product_id}}', product)
|
||||
return res
|
||||
6
sf_message/models/sf_message_stock_picking.py
Normal file
6
sf_message/models/sf_message_stock_picking.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessageStockPicking(models.Model):
|
||||
_name = 'stock.picking'
|
||||
_inherit = ['stock.picking', 'jikimo.message.dispatch']
|
||||
@@ -1,48 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class SfMessageTemplate(models.Model):
|
||||
_name = "sf.message.template"
|
||||
_description = u'消息模板'
|
||||
_inherit = "jikimo.message.template"
|
||||
|
||||
name = fields.Char(string=u"名称", required=True)
|
||||
type = fields.Selection([
|
||||
('待接单', '待接单'),
|
||||
('待排程', '待排程'),
|
||||
('坯料采购', '坯料采购'),
|
||||
('坯料发料', '坯料发料'),
|
||||
('待编程', '待编程'),
|
||||
('调拨入库', '调拨入库'),
|
||||
('功能刀具组装', '功能刀具组装'),
|
||||
('功能刀具寿命到期', '功能刀具寿命到期'),
|
||||
('程序用刀计划异常', '程序用刀计划异常'),
|
||||
('工单无CNC程序', '工单无CNC程序'),
|
||||
('生产线无功能刀具', '生产线无功能刀具'),
|
||||
('工单无定位数据', '工单无定位数据'),
|
||||
('工单FTP无文件', '工单FTP无文件'),
|
||||
('工单加工失败', '工单加工失败'),
|
||||
('设备故障及异常', '设备故障及异常'),
|
||||
('工单逾期预警', '工单逾期预警'),
|
||||
('工单已逾期', '工单已逾期'),
|
||||
('销售订单逾期', '销售订单逾期'),
|
||||
('销售订单已逾期', '销售订单已逾期'),
|
||||
('待质量判定', '待质量判定'),
|
||||
('生产完工待入库', '生产完工待入库'),
|
||||
('订单发货', '订单发货')
|
||||
], string='类型', required=True)
|
||||
description = fields.Char(string=u"描述")
|
||||
content = fields.Html(string=u"内容", render_engine='qweb', translate=True, prefetch=True, sanitize=False)
|
||||
msgtype = fields.Selection(
|
||||
[('text', u'文字'), ('markdown', u'Markdown')], u'消息类型',
|
||||
required=True, default='markdown')
|
||||
notification_department_id = fields.Many2one('hr.department', u'通知部门', required=True)
|
||||
notification_employee_ids = fields.Many2many('hr.employee', string=u'员工',
|
||||
domain="[('department_id', '=',notification_department_id)]",
|
||||
required=True)
|
||||
active = fields.Boolean(string=u"是否有效", default=True)
|
||||
|
||||
@api.onchange('notification_department_id')
|
||||
def _clear_employee_ids(self):
|
||||
if self.notification_department_id:
|
||||
self.notification_employee_ids = False
|
||||
def _get_message_model(self):
|
||||
res = super(SfMessageTemplate, self)._get_message_model()
|
||||
res.append("sale.order")
|
||||
return res
|
||||
|
||||
6
sf_message/models/sf_message_workorder.py
Normal file
6
sf_message/models/sf_message_workorder.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessageWork(models.Model):
|
||||
_name = 'mrp.workorder'
|
||||
_inherit = ['mrp.workorder', 'jikimo.message.dispatch']
|
||||
@@ -1,9 +1,23 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sf_message_template_group_sale_salemanager,sf_message_template,model_sf_message_template,sf_base.group_sale_salemanager,1,1,1,0
|
||||
access_sf_message_template_group_purchase,sf_message_template,model_sf_message_template,sf_base.group_purchase,1,1,1,0
|
||||
access_sf_message_template_group_sf_stock_user,sf_message_template,model_sf_message_template,sf_base.group_sf_stock_user,1,1,1,0
|
||||
access_sf_message_template_group_sf_order_user,sf_message_template,model_sf_message_template,sf_base.group_sf_order_user,1,1,1,0
|
||||
access_sf_message_template_group_sf_tool_user,sf_message_template,model_sf_message_template,sf_base.group_sf_tool_user,1,1,1,0
|
||||
|
||||
access_jikimo_message_template_group_sale_salemanager,jikimo_message_template,model_jikimo_message_template,sf_base.group_sale_salemanager,1,1,1,0
|
||||
access_jikimo_message_template_group_purchase,jikimo_message_template,model_jikimo_message_template,sf_base.group_purchase,1,1,1,0
|
||||
access_jikimo_message_template_group_sf_stock_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_stock_user,1,1,1,0
|
||||
access_jikimo_message_template_group_sf_order_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_order_user,1,1,1,0
|
||||
access_jikimo_message_template_group_sf_tool_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_tool_user,1,1,1,0
|
||||
|
||||
access_jikimo_message_bussiness_node_group_sale_salemanager,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sale_salemanager,1,1,1,0
|
||||
access_jikimo_message_bussiness_node_group_purchase,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_purchase,1,1,1,0
|
||||
access_jikimo_message_bussiness_node_group_sf_stock_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_stock_user,1,1,1,0
|
||||
access_jikimo_message_bussiness_node_group_sf_order_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_order_user,1,1,1,0
|
||||
access_jikimo_message_bussiness_node_group_sf_tool_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_tool_user,1,1,1,0
|
||||
|
||||
access_jikimo_message_queue_group_sale_salemanager,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sale_salemanager,1,1,1,0
|
||||
access_jikimo_message_queue_group_purchase,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_purchase,1,1,1,0
|
||||
access_jikimo_message_queue_group_sf_stock_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_stock_user,1,1,1,0
|
||||
access_jikimo_message_queue_group_sf_order_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_order_user,1,1,1,0
|
||||
access_jikimo_message_queue_group_sf_tool_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_tool_user,1,1,1,0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
<record id="sf_message_template_view_form" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.view.form</field>
|
||||
<field name="model">sf.message.template</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="消息模板">
|
||||
<sheet>
|
||||
@@ -18,12 +18,12 @@
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<field name="type"/>
|
||||
<!-- <field name="type"/>-->
|
||||
<field name="notify_model_id"/>
|
||||
<field name="content" widget="html" class="oe-bordered-editor"
|
||||
options="{'style-inline': true, 'codeview': true, 'dynamic_placeholder': true}"/>
|
||||
<field name="description"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="type"/>
|
||||
<field name="notification_department_id"/>
|
||||
<field name="notification_employee_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
@@ -34,14 +34,13 @@
|
||||
|
||||
<record id="sf_message_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.view.tree</field>
|
||||
<field name="model">sf.message.template</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="消息模板">
|
||||
<field name="name"/>
|
||||
<field name="type"/>
|
||||
<!-- <field name="type"/>-->
|
||||
<field name="content"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="type"/>
|
||||
<field name="notification_department_id"/>
|
||||
<field name="notification_employee_ids" widget="many2many_tags"/>
|
||||
<field name="description"/>
|
||||
@@ -51,11 +50,11 @@
|
||||
|
||||
<record id="sf_message_template_search_view" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.search.view</field>
|
||||
<field name="model">sf.message.template</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" string="模糊搜索"
|
||||
filter_domain="['|','|',('name','like',self),('type','like',self),('description','like',self)]"/>
|
||||
filter_domain="['|','|',('name','like',self),('description','like',self)]"/>
|
||||
<field name="name"/>
|
||||
<filter name="filter_active" string="已归档" domain="[('active','=',False)]"/>
|
||||
</search>
|
||||
@@ -65,7 +64,7 @@
|
||||
<!--定义单证类型视图动作-->
|
||||
<record id="sf_message_template_action" model="ir.actions.act_window">
|
||||
<field name="name">消息模板</field>
|
||||
<field name="res_model">sf.message.template</field>
|
||||
<field name="res_model">message.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="sf_message_template_view_tree"/>
|
||||
</record>
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
""",
|
||||
'category': 'sf',
|
||||
'website': 'https://www.sf.cs.jikimo.com',
|
||||
'depends': ['sf_base', 'base_setup'],
|
||||
'depends': ['sf_base', 'base_setup','sf_bf_connect'],
|
||||
'data': [
|
||||
'data/ir_cron_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/res_config_settings_views.xml'
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/order_price.xml',
|
||||
],
|
||||
'demo': [
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from . import ftp_operate
|
||||
from . import res_config_setting
|
||||
from . import sync_common
|
||||
from . import order_price
|
||||
29
sf_mrs_connect/models/order_price.py
Normal file
29
sf_mrs_connect/models/order_price.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class OrderPrice(models.Model):
|
||||
_name = 'order.price'
|
||||
_description = '订单价格对比'
|
||||
sale_order_id = fields.Many2one('sale.order', '销售订单')
|
||||
bfm_order_name = fields.Char(related="sale_order_id.default_code", string='bfm订单号')
|
||||
sale_order_name = fields.Char(related="sale_order_id.name", string='销售订单号')
|
||||
currency_id = fields.Many2one(
|
||||
related='sale_order_id.currency_id', string='货币', store=True)
|
||||
sale_order_amount_total = fields.Monetary(related="sale_order_id.amount_total", tracking=4, string='销售订单金额')
|
||||
bfm_amount_total = fields.Float(string='价格合计', compute='_compute_bfm_amount_total', store=True)
|
||||
|
||||
def is_float(self,value):
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
@api.depends('sale_order_id.remark')
|
||||
def _compute_bfm_amount_total(self):
|
||||
for record in self:
|
||||
amount_total = 0
|
||||
for line in record.sale_order_id.order_line:
|
||||
# 判断remark是否存在并且是否是数字
|
||||
if line.remark and self.is_float(line.remark):
|
||||
amount_total += float(line.remark)
|
||||
record.bfm_amount_total = amount_total
|
||||
@@ -16,13 +16,10 @@ class ResConfigSettings(models.TransientModel):
|
||||
token = fields.Char(string='TOKEN', default='b811ac06-3f00-11ed-9aed-0242ac110003')
|
||||
sf_secret_key = fields.Char(string='密钥', default='wBmxej38OkErKhD6')
|
||||
sf_url = fields.Char(string='访问地址', default='https://sf.cs.jikimo.com')
|
||||
agv_rcs_url = fields.Char(string='avg_rcs访问地址',
|
||||
default='http://172.16.10.114:8182/rcms/services/rest/hikRpcService/genAgvSchedulingTask')
|
||||
center_control_url = fields.Char(string='中控访问地址',
|
||||
default='http://172.16.21.50:8001')
|
||||
center_control_Authorization = fields.Char(string='中控访问认证')
|
||||
wbcode = fields.Char('地码')
|
||||
agv_code = fields.Char(string='agv编号')
|
||||
|
||||
task_type_no = fields.Char('任务单类型编号')
|
||||
model_parser_url = fields.Char('特征识别路径')
|
||||
ftp_host = fields.Char(string='FTP的ip')
|
||||
@@ -103,9 +100,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
token = config.get_param('token', default='')
|
||||
sf_secret_key = config.get_param('sf_secret_key', default='')
|
||||
sf_url = config.get_param('sf_url', default='')
|
||||
agv_rcs_url = config.get_param('agv_rcs_url', default='')
|
||||
wbcode = config.get_param('wbcode', default='')
|
||||
agv_code = config.get_param('agv_code', default='')
|
||||
|
||||
center_control_url = config.get_param('center_control_url', default='')
|
||||
center_control_Authorization = config.get_param('center_control_Authorization', default='')
|
||||
ftp_host = config.get_param('ftp_host', default='')
|
||||
@@ -118,9 +113,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
token=token,
|
||||
sf_secret_key=sf_secret_key,
|
||||
sf_url=sf_url,
|
||||
agv_rcs_url=agv_rcs_url,
|
||||
wbcode=wbcode,
|
||||
agv_code=agv_code,
|
||||
|
||||
center_control_url=center_control_url,
|
||||
center_control_Authorization=center_control_Authorization,
|
||||
ftp_host=ftp_host,
|
||||
@@ -137,9 +130,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
ir_config.set_param("token", self.token or "")
|
||||
ir_config.set_param("sf_secret_key", self.sf_secret_key or "")
|
||||
ir_config.set_param("sf_url", self.sf_url or "")
|
||||
ir_config.set_param("agv_rcs_url", self.agv_rcs_url or "")
|
||||
ir_config.set_param("wbcode", self.wbcode or "")
|
||||
ir_config.set_param("agv_code", self.agv_code or "")
|
||||
|
||||
ir_config.set_param("center_control_url", self.center_control_url or "")
|
||||
ir_config.set_param("center_control_Authorization", self.center_control_Authorization or "")
|
||||
ir_config.set_param("ftp_host", self.ftp_host or "")
|
||||
@@ -184,7 +175,10 @@ class ResConfigSettings(models.TransientModel):
|
||||
new_price = res_order_lines_map.get(str(index))
|
||||
if order_line:
|
||||
# 修改单价
|
||||
order_line.write({'remark': new_price})
|
||||
order_line.write({'remark': new_price*order_line.product_uom_qty})
|
||||
order_price = self.env['order.price'].sudo().search([('sale_order_id', '=',need_change_sale_order.id )])
|
||||
if not order_price:
|
||||
self.env['order.price'].sudo().create({'sale_order_id':need_change_sale_order.id})
|
||||
else:
|
||||
logging.error('同步销售订单价格失败 {}'.format(response.text))
|
||||
raise UserError('同步销售订单价格失败')
|
||||
|
||||
@@ -2438,6 +2438,7 @@ class CuttingToolBasicParameters(models.Model):
|
||||
'handle_length': integral_tool_item['shank_length'],
|
||||
'blade_tip_diameter': integral_tool_item['tip_diameter'],
|
||||
'blade_tip_working_size': integral_tool_item['tip_handling_size'],
|
||||
'tip_r_size': integral_tool_item['tip_r_size'],
|
||||
'blade_tip_taper': integral_tool_item['knife_tip_taper'],
|
||||
'blade_helix_angle': integral_tool_item['blade_helix_angle'],
|
||||
'blade_width': integral_tool_item['blade_width'],
|
||||
@@ -2459,6 +2460,7 @@ class CuttingToolBasicParameters(models.Model):
|
||||
'handle_length': integral_tool_item['shank_length'],
|
||||
'blade_tip_diameter': integral_tool_item['tip_diameter'],
|
||||
'blade_tip_working_size': integral_tool_item['tip_handling_size'],
|
||||
'tip_r_size': integral_tool_item['tip_r_size'],
|
||||
'blade_tip_taper': integral_tool_item['knife_tip_taper'],
|
||||
'blade_helix_angle': integral_tool_item['blade_helix_angle'],
|
||||
'blade_width': integral_tool_item['blade_width'],
|
||||
@@ -2789,6 +2791,7 @@ class CuttingToolBasicParameters(models.Model):
|
||||
'handle_length': integral_tool_item['shank_length'],
|
||||
'blade_tip_diameter': integral_tool_item['tip_diameter'],
|
||||
'blade_tip_working_size': integral_tool_item['tip_handling_size'],
|
||||
'tip_r_size': integral_tool_item['tip_r_size'],
|
||||
'blade_tip_taper': integral_tool_item['knife_tip_taper'],
|
||||
'blade_helix_angle': integral_tool_item['blade_helix_angle'],
|
||||
'blade_width': integral_tool_item['blade_width'],
|
||||
@@ -2810,6 +2813,7 @@ class CuttingToolBasicParameters(models.Model):
|
||||
'handle_length': integral_tool_item['shank_length'],
|
||||
'blade_tip_diameter': integral_tool_item['tip_diameter'],
|
||||
'blade_tip_working_size': integral_tool_item['tip_handling_size'],
|
||||
'tip_r_size': integral_tool_item['tip_r_size'],
|
||||
'blade_tip_taper': integral_tool_item['knife_tip_taper'],
|
||||
'blade_helix_angle': integral_tool_item['blade_helix_angle'],
|
||||
'blade_width': integral_tool_item['blade_width'],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sf_static_resource_datasync,sf_static_resource_datasync,model_sf_static_resource_datasync,base.group_user,1,1,1,1
|
||||
|
||||
|
||||
access_order_price,order.price,model_order_price,base.group_user,1,1,1,1
|
||||
|
||||
|
||||
|
||||
|
||||
|
27
sf_mrs_connect/views/order_price.xml
Normal file
27
sf_mrs_connect/views/order_price.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.actions.act_window" id="order_price_tree_act">
|
||||
<field name="name">bfm订单价格对比</field>
|
||||
<field name="res_model">order.price</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<menuitem sequence="22" name="销售订单bfm对比" id="menu_sale_order_bfm_price"
|
||||
action="order_price_tree_act"
|
||||
parent="sale.sale_order_menu"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
|
||||
<record id="view_order_price_tree" model="ir.ui.view">
|
||||
<field name="name">order.price.list</field>
|
||||
<field name="model">order.price</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="订单计划">
|
||||
<field name="bfm_order_name"/>
|
||||
<field name="sale_order_name"/>
|
||||
<field name="sale_order_amount_total"/>
|
||||
<field name="bfm_amount_total"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -74,28 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>AGV参数配置</h2>
|
||||
<div class="row mt16 o_settings_container" id="agv_config">
|
||||
<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="agv_rcs_url" string="访问地址"/>
|
||||
<field name="agv_rcs_url"/>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
<label for="agv_code" string="车辆编号"/>
|
||||
<field name="agv_code"/>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
<label for="wbcode"/>
|
||||
<field name="wbcode"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>中控参数配置</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
|
||||
@@ -70,6 +70,32 @@ class sf_production_plan(models.Model):
|
||||
sequence = fields.Integer(string='序号', copy=False, readonly=True, index=True)
|
||||
current_operation_name = fields.Char(string='当前工序名称', size=64, default='生产计划')
|
||||
|
||||
@api.onchange('date_planned_start')
|
||||
def date_planned_start_onchange(self):
|
||||
if self.date_planned_start:
|
||||
self.date_planned_finished = self.date_planned_start + timedelta(hours=1)
|
||||
|
||||
#处理计划状态非待排程,计划结束时间为空的数据处理
|
||||
def deal_no_date_planned_finished(self):
|
||||
plans = self.env['sf.production.plan'].search(
|
||||
[('date_planned_finished', '=', False), ('state', 'in', ['processing', 'done', 'finished'])])
|
||||
for item in plans:
|
||||
if item.date_planned_start:
|
||||
item.date_planned_finished = item.date_planned_start + timedelta(hours=1)
|
||||
|
||||
# 处理计划订单截止时间为空的数据
|
||||
def deal_no_order_deadline(self):
|
||||
plans = self.env['sf.production.plan'].sudo().search(
|
||||
[('order_deadline', '=', False)])
|
||||
for item in plans:
|
||||
if item.date_planned_start:
|
||||
item.order_deadline = item.date_planned_start + timedelta(days=7)
|
||||
@api.model
|
||||
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None):
|
||||
|
||||
info = super(sf_production_plan, self).search_read(domain, fields, offset, limit, order)
|
||||
return info
|
||||
|
||||
# 计算实际加工时长
|
||||
@api.depends('actual_start_time', 'actual_end_time')
|
||||
def _compute_actual_process_time(self):
|
||||
@@ -192,7 +218,7 @@ class sf_production_plan(models.Model):
|
||||
|
||||
return num
|
||||
|
||||
def do_production_schedule(self, date_planned_start):
|
||||
def do_production_schedule(self):
|
||||
"""
|
||||
排程方法
|
||||
"""
|
||||
@@ -200,8 +226,7 @@ class sf_production_plan(models.Model):
|
||||
if not record.production_line_id:
|
||||
raise ValidationError("未选择生产线")
|
||||
else:
|
||||
|
||||
is_schedule = self.deal_processing_schedule(date_planned_start)
|
||||
is_schedule = self.deal_processing_schedule(record.date_planned_start)
|
||||
if not is_schedule:
|
||||
raise ValidationError("排程失败")
|
||||
workorder_id_list = record.production_id.workorder_ids.ids
|
||||
@@ -210,7 +235,6 @@ class sf_production_plan(models.Model):
|
||||
for item in record.production_id.workorder_ids:
|
||||
if item.name == 'CNC加工':
|
||||
item.date_planned_finished = datetime.now() + timedelta(days=100)
|
||||
# item.date_planned_start = record.date_planned_start
|
||||
item.date_planned_start = self.date_planned_start if self.date_planned_start else datetime.now()
|
||||
record.sudo().production_id.plan_start_processing_time = item.date_planned_start
|
||||
item.date_planned_finished = item.date_planned_start + timedelta(
|
||||
@@ -223,6 +247,8 @@ class sf_production_plan(models.Model):
|
||||
record.date_planned_start, record.date_planned_finished = \
|
||||
item.date_planned_start, item.date_planned_finished
|
||||
record.state = 'done'
|
||||
record.date_planned_finished = record.date_planned_start + timedelta(
|
||||
minutes=60) if not record.date_planned_finished else record.date_planned_finished
|
||||
# record.production_id.schedule_state = '已排'
|
||||
record.sudo().production_id.schedule_state = '已排'
|
||||
record.sudo().production_id.process_state = '待装夹'
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 673 B |
@@ -88,8 +88,8 @@
|
||||
|
||||
<group string="加工信息">
|
||||
|
||||
<field name="date_planned_start" placeholder="如果不选择计划开始时间,会取当前时间来做排程"/>
|
||||
<field name="date_planned_finished"/>
|
||||
<field name="date_planned_start" placeholder="如果不选择计划开始时间,会取当前时间来做排程" required="1"/>
|
||||
<field name="date_planned_finished" required="1"/>
|
||||
<field name="actual_process_time"/>
|
||||
<field name="actual_start_time"/>
|
||||
<field name="actual_end_time"/>
|
||||
@@ -278,7 +278,6 @@
|
||||
sequence="150"
|
||||
action="sf_production_plan_action"
|
||||
groups="sf_base.group_plan_dispatch"
|
||||
web_icon="sf_plan,static/description/计划.png"
|
||||
/>
|
||||
|
||||
<!-- <record model="ir.ui.menu" id="mrp_custom_menu" inherit_id="mrp.menu_mrp_manufacturing"> -->
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime
|
||||
from odoo import fields, models
|
||||
# from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,10 +15,15 @@ class Action_Plan_All_Wizard(models.TransientModel):
|
||||
_name = 'sf.action.plan.all.wizard'
|
||||
_description = u'排程向导'
|
||||
|
||||
def _get_date_planned_start(self):
|
||||
planned_start_date = datetime.now() + timedelta(minutes=10)
|
||||
logging.info('计划开始时间: %s', planned_start_date)
|
||||
return planned_start_date
|
||||
|
||||
# 选择生产线
|
||||
production_line_id = fields.Many2one('sf.production.line', string=u'生产线', required=True)
|
||||
date_planned_start = fields.Datetime(string='计划开始时间', index=True, copy=False,
|
||||
default=fields.Datetime.now)
|
||||
default=_get_date_planned_start)
|
||||
|
||||
# 接收传递过来的计划ID
|
||||
plan_ids = fields.Many2many('sf.production.plan', string=u'计划ID')
|
||||
@@ -36,7 +42,7 @@ class Action_Plan_All_Wizard(models.TransientModel):
|
||||
plan_obj = self.env['sf.production.plan'].browse(plan.id)
|
||||
plan_obj.production_line_id = self.production_line_id.id
|
||||
plan.date_planned_start = self.date_planned_start
|
||||
plan_obj.do_production_schedule(self.date_planned_start)
|
||||
plan_obj.do_production_schedule()
|
||||
# plan_obj.state = 'done'
|
||||
print('处理计划:', plan.id, '完成')
|
||||
|
||||
|
||||
@@ -58,6 +58,15 @@ class QuickEasyOrder(models.Model):
|
||||
|
||||
part_drawing_number = fields.Char('零件图号')
|
||||
machining_drawings = fields.Binary('2D加工图纸')
|
||||
machining_drawings_name = fields.Char('2D加工图纸名')
|
||||
|
||||
@api.onchange('machining_drawings_name')
|
||||
def _onchange_machining_drawings_name(self):
|
||||
for item in self:
|
||||
if item.machining_drawings_name:
|
||||
if not item.machining_drawings_name.lower().endswith(
|
||||
'.pdf'):
|
||||
raise ValidationError('文件格式上传有误,请检查文件后缀(不区分大小写)是否为pdf')
|
||||
|
||||
@api.onchange('parameter_ids')
|
||||
def _compute_parameter_ids(self):
|
||||
@@ -128,6 +137,10 @@ class QuickEasyOrder(models.Model):
|
||||
if len(item.upload_model_file) > 1:
|
||||
raise ValidationError('只允许上传一个文件')
|
||||
if item.upload_model_file:
|
||||
if not item.upload_model_file.name.lower().endswith(
|
||||
'.step') and not item.upload_model_file.name.lower().endswith(
|
||||
'.stp'):
|
||||
raise ValidationError('文件格式上传有误,请检查文件后缀(不区分大小写)是否为step、stp')
|
||||
file_attachment_id = item.upload_model_file[0]
|
||||
# 附件路径
|
||||
report_path = file_attachment_id._full_path(file_attachment_id.store_fname)
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
<field name="unit_price"/>
|
||||
<field name="price" options="{'format': false}"/>
|
||||
<field name="part_drawing_number"/>
|
||||
<field name="machining_drawings" widget="pdf_viewer"/>
|
||||
<field name="machining_drawings" filename="machining_drawings_name" widget="pdf_viewer"/>
|
||||
<field name="machining_drawings_name" invisible="1"/>
|
||||
<field name="sale_order_id"
|
||||
attrs='{"invisible": [("sale_order_id","=",False)],"readonly": [("sale_order_id","!=",False)]}'/>
|
||||
</group>
|
||||
|
||||
@@ -357,6 +357,8 @@ class FunctionalToolAssembly(models.Model):
|
||||
"""
|
||||
智能工厂组装单处扫码校验刀具物料
|
||||
"""
|
||||
if 'O-CMD' in barcode:
|
||||
return ''
|
||||
for record in self:
|
||||
tool_assembly_id = self.env['sf.functional.tool.assembly'].browse(self.ids)
|
||||
lot_ids = self.env['stock.lot'].sudo().search([('rfid', '=', barcode)])
|
||||
@@ -835,7 +837,7 @@ class FunctionalToolAssembly(models.Model):
|
||||
|
||||
if options == '刀柄+整体式刀具':
|
||||
if not integral_ids:
|
||||
raise ValidationError('功能刀具清单的BOM未配置[刀柄]信息,请先配置BOM再开始组装!')
|
||||
raise ValidationError('功能刀具清单的BOM未配置[整体式刀具]信息,请先配置BOM再开始组装!')
|
||||
return {'options': options, 'handle_ids': handle_ids, 'integral_ids': integral_ids}
|
||||
elif options == '刀柄+刀杆+刀片':
|
||||
if not blade_ids:
|
||||
|
||||
@@ -3,4 +3,4 @@ from odoo import models, fields
|
||||
|
||||
class SyncFunctionalCuttingToolModel(models.Model):
|
||||
_inherit = 'sf.functional.cutting.tool.model'
|
||||
cutting_tool_type_ids = fields.Many2many('sf.cutting.tool.type', string='适用刀具物料类型', required=True)
|
||||
cutting_tool_type_ids = fields.Many2many('sf.cutting.tool.type', string='适用刀具物料类型')
|
||||
@@ -30,13 +30,27 @@ class jikimo_bom(models.Model):
|
||||
return result
|
||||
|
||||
def check_types_in_list(self):
|
||||
# 统计每个元素的类型
|
||||
type_counts = Counter(item.cutting_tool_material_id.name for item in self.product_ids)
|
||||
return all(count > 0 for count in type_counts.values()) and len(type_counts) == self.options.split('+')
|
||||
"""
|
||||
检查产品列表中的元素是否包含了所有指定的类型,并且每种类型至少出现一次。
|
||||
:return: 如果条件满足返回True,否则返回False
|
||||
"""
|
||||
if not self.product_ids:
|
||||
return False
|
||||
try:
|
||||
# 统计每个类型的出现次数
|
||||
type_counts = Counter(item.cutting_tool_material_id.name for item in self.product_ids)
|
||||
|
||||
# 检查是否每种类型的出现次数都大于0,并且类型的数量与选项字符串中的数量相等
|
||||
return all(count > 0 for count in type_counts.values()) and len(type_counts) == len(self.options.split('+'))
|
||||
except AttributeError:
|
||||
# 如果出现属性错误,说明产品列表中的元素可能缺少必要的属性
|
||||
return False
|
||||
# type_counts = Counter(item.cutting_tool_material_id.name for item in self.product_ids)
|
||||
# return all(count > 0 for count in type_counts.values()) and len(type_counts) == self.options.split('+')
|
||||
|
||||
def write(self, vals):
|
||||
# 在更新模型时记录旧的 Many2many ID 列表
|
||||
if 'product_ids' in vals:
|
||||
if 'product_ids' in vals and not self.env.context.get('is_assembly_options'):
|
||||
old_product_counter = Counter(self.product_ids.ids)
|
||||
super(jikimo_bom, self).write(vals)
|
||||
new_product_counter = Counter(self.product_ids.ids)
|
||||
@@ -47,9 +61,15 @@ class jikimo_bom(models.Model):
|
||||
return True
|
||||
else:
|
||||
raise UserError('每种物料最少要有一个')
|
||||
return True
|
||||
return super(jikimo_bom, self).write(vals)
|
||||
|
||||
def bom_product_domains(self, assembly_options):
|
||||
"""
|
||||
根据装配选项生成产品域列表
|
||||
:param assembly_options: 装配选项字符串,各选项以'+'分隔
|
||||
:return: 动态生成的产品搜索条件
|
||||
"""
|
||||
self.options = assembly_options
|
||||
cutting_tool_materials = self.env['sf.cutting.tool.material'].search(
|
||||
[('name', 'in', assembly_options.split('+'))])
|
||||
@@ -82,23 +102,25 @@ class jikimo_bom(models.Model):
|
||||
domains = domains + domain
|
||||
if index != 0:
|
||||
domains = ['|'] + domains
|
||||
# wqwqwe = self.env['product.product'].search(ddd)
|
||||
# product = self.env['product.product'].search(domain)
|
||||
# if product:
|
||||
# products = products + product
|
||||
domains = domains + [('stock_move_count', '>', 0)]
|
||||
domains = domains + [('stock_move_ids', '!=',False)]
|
||||
return domains
|
||||
|
||||
def generate_bill_materials(self, assembly_options):
|
||||
"""
|
||||
生成物料清单
|
||||
|
||||
根据装配选项生成物料清单,首先获取产品领域,然后搜索相关产品,并设置产品ID。
|
||||
|
||||
:param assembly_options: 组装方式
|
||||
:type assembly_options: 装配选项字符串,各选项以'+'分隔
|
||||
"""
|
||||
domains = self.bom_product_domains(assembly_options)
|
||||
products = self.env['product.product'].search(domains)
|
||||
if products:
|
||||
self.product_ids = [Command.set(products.ids)]
|
||||
# if option.name == '刀盘':
|
||||
# hilt = self.env['product.product'].search(
|
||||
# [('cutting_tool_blade_diameter', '=', self.tool_inventory_id.diameter),
|
||||
# ('cutting_tool_material_id', '=', option.id)])
|
||||
# self.product_ids = [Command.set(hilt.ids)]k
|
||||
new_context = dict(self.env.context)
|
||||
new_context['is_assembly_options'] = True
|
||||
self.with_context(new_context).write({'product_ids': [Command.set(products.ids)]})
|
||||
# self.product_ids = [Command.set(products.ids)]
|
||||
|
||||
|
||||
class jikimo_bom_line(models.Model):
|
||||
@@ -111,15 +133,15 @@ class jikimo_bom_line(models.Model):
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
_order = 'cutting_tool_material_id, cutting_tool_type_id'
|
||||
stock_move_count = fields.Integer(string='stock_move count', compute='_compute_stock_move_count', store=True)
|
||||
|
||||
@api.depends('stock_move_ids')
|
||||
def _compute_stock_move_count(self):
|
||||
for record in self:
|
||||
if record.stock_move_ids:
|
||||
record.stock_move_count = len(record.stock_move_ids)
|
||||
else:
|
||||
record.stock_move_count = 0
|
||||
# stock_move_count = fields.Integer(string='stock_move count', compute='_compute_stock_move_count')
|
||||
#
|
||||
# @api.depends('stock_move_ids')
|
||||
# def _compute_stock_move_count(self):
|
||||
# for record in self:
|
||||
# if record.stock_move_ids:
|
||||
# record.stock_move_count = len(record.stock_move_ids)
|
||||
# else:
|
||||
# record.stock_move_count = 0
|
||||
|
||||
def search(self, args, offset=0, limit=None, order=None, count=False):
|
||||
# 你可以在这里修改 `args` 以调整搜索条件
|
||||
|
||||
@@ -298,8 +298,8 @@ class SfShelfLocationLot(models.Model):
|
||||
brand_id = fields.Many2one('sf.machine.brand', '品牌', related='product_id.brand_id')
|
||||
|
||||
cutting_tool_blade_diameter = fields.Float('刃部直径(mm)', related='product_id.cutting_tool_blade_diameter')
|
||||
cutting_tool_blade_tip_working_size = fields.Char('刀尖R角(mm)',
|
||||
related='product_id.cutting_tool_blade_tip_working_size')
|
||||
cutting_tool_blade_tip_working_size = fields.Float('刀尖R角(mm)',
|
||||
related='product_id.cutting_tool_blade_tip_r_size')
|
||||
cutting_tool_blade_radius = fields.Char('刀尖圆弧半径(mm)',
|
||||
related='product_id.cutting_tool_blade_tip_circular_arc_radius')
|
||||
cutting_tool_cutter_arbor_diameter = fields.Float('刀杆直径(mm)',
|
||||
|
||||
@@ -14,10 +14,7 @@ class ToolInventory(models.Model):
|
||||
self._bom_mainfest()
|
||||
return self.bom_mainfest()
|
||||
request.session['jikimo_bom_product'] = {'bom_id': int(self.jikimo_bom_ids)}
|
||||
# context = dict(self.env.context)
|
||||
# context.update({'jikimo_bom_product': self.jikimo_bom_ids.options})
|
||||
# if self.functional_cutting_tool_model_id.cutting_tool_type_ids:
|
||||
# context.update({'jikimo_bom_product_cutting_tool_type': self.functional_cutting_tool_model_id.cutting_tool_type_ids.ids})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': '刀具组装清单',
|
||||
@@ -26,7 +23,6 @@ class ToolInventory(models.Model):
|
||||
'view_id': self.env.ref('sf_tool_management.view_jikimo_bom_form').id,
|
||||
'res_id': int(self.jikimo_bom_ids),
|
||||
'target': 'current', # Use 'new' to open in a new window/tab
|
||||
# {'jikimo_bom_product': self.jikimo_bom_ids.options}
|
||||
}
|
||||
|
||||
# 创建bom单
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<field name="name">jikimo.bom.form</field>
|
||||
<field name="model">jikimo.bom</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<form create="False">
|
||||
<header>
|
||||
<button type="action" name="%(action_jikimo_bom_wizard)d"
|
||||
class="btn btn-info" string="组装方式.." context="{'default_bom_id':id}"
|
||||
@@ -31,7 +31,7 @@
|
||||
<notebook colspan="4">
|
||||
<page string="物料清单">
|
||||
<field name="product_ids" context="{'jikimo_bom_product': True}">
|
||||
<tree>
|
||||
<tree create="False">
|
||||
<field name="name"/>
|
||||
<!-- <field name="categ_id"/>-->
|
||||
<field name="cutting_tool_material_id"/>
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree create="0" export_xlsx="0" delete="0">
|
||||
<header>
|
||||
<button string="确认" name="set_tool_material" type="object"
|
||||
class="treeHeaderBtn"/>
|
||||
<button string="确认" name="set_tool_material" type="object" class="treeHeaderBtn"/>
|
||||
</header>
|
||||
<field name="name"/>
|
||||
<field name="cutting_tool_type_id"/>
|
||||
@@ -62,6 +61,7 @@
|
||||
<field name="brand_id"/>
|
||||
<field name="shelf_location_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="qty"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -86,6 +86,7 @@
|
||||
<field name="brand_id"/>
|
||||
<field name="shelf_location_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="qty"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -111,6 +112,7 @@
|
||||
<field name="brand_id"/>
|
||||
<field name="shelf_location_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="qty"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -135,6 +137,7 @@
|
||||
<field name="brand_id"/>
|
||||
<field name="shelf_location_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="qty"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -478,9 +478,9 @@
|
||||
class="btn-primary"/>
|
||||
<button string="确认组装" name="functional_tool_assembly" type="object"
|
||||
attrs="{'invisible': [('assemble_status', 'not in', ['01'])]}"
|
||||
class="btn-primary"/>
|
||||
class="btn-primary jikimo_button_confirm"/>
|
||||
<button name="get_tool_preset_parameter" string="获取测量值"
|
||||
type="object" class="btn-primary"
|
||||
type="object" class="btn-primary jikimo_button_flushed"
|
||||
attrs="{'invisible': [('assemble_status', 'in', ['0','1','2'])]}"
|
||||
/>
|
||||
<field name="assemble_status" widget="statusbar" statusbar_visible="0,01,1"/>
|
||||
|
||||
@@ -6,10 +6,32 @@
|
||||
<field name="inherit_id" ref="sf_base.view_tool_inventory_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='extension']" position="before">
|
||||
<field name="knife_handle_model" />
|
||||
<field name="knife_handle_model" class="o-sticky-header"/>
|
||||
<button name="bom_mainfest" string="bom清单" type="object" class="btn-link"
|
||||
icon="fa-refresh" />
|
||||
icon="fa-refresh"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_tool_inventory_inherit_search" model="ir.ui.view">
|
||||
<field name="name">sf.tool.inventory.inherit.search</field>
|
||||
<field name="model">sf.tool.inventory</field>
|
||||
<field name="inherit_id" ref="sf_base.view_cutting_tool_inventory_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='extension']" position="after">
|
||||
<searchpanel>
|
||||
<field name="functional_cutting_tool_model_id" enable_counters="1"/>
|
||||
<!-- <field name="job_id" enable_counters="1"/>-->
|
||||
<!-- <field name="department_id" enable_counters="1"/>-->
|
||||
<!-- <field name="company_id" enable_counters="1"/>-->
|
||||
</searchpanel>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<!-- <searchpanel>-->
|
||||
<!-- <field name="org_type_id_display" enable_counters="1"/>-->
|
||||
<!-- <!– <field name="job_id" enable_counters="1"/>–>-->
|
||||
<!-- <field name="department_id" enable_counters="1"/>-->
|
||||
<!-- <!– <field name="company_id" enable_counters="1"/>–>-->
|
||||
<!-- </searchpanel>-->
|
||||
</odoo>
|
||||
@@ -8,7 +8,7 @@
|
||||
<field name="inherit_id" ref="sf_base.view_cutter_function_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="cutting_tool_type_ids" widget="many2many_tags"/>
|
||||
<field name="cutting_tool_type_ids" widget="many2many_tags" options="{'no_create': True}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -15,9 +15,24 @@ class JikimoBomWizard(models.TransientModel):
|
||||
('刀柄+刀杆+刀片', '刀柄+刀杆+刀片'),
|
||||
('刀柄+刀盘+刀片', '刀柄+刀盘+刀片')
|
||||
], string='组装方式', required=True)
|
||||
# assembly_options_ids = fields.Many2many('sf.cutting.tool.material', string="组装方式")
|
||||
is_ok = fields.Boolean('确认上述信息正确无误。')
|
||||
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(JikimoBomWizard, self).default_get(fields)
|
||||
# 根据某个字段的值设置默认选项
|
||||
if 'default_bom_id' in self.env.context:
|
||||
jikimo_bom = self.env['jikimo.bom'].browse(self.env.context['default_bom_id'])
|
||||
if not jikimo_bom:
|
||||
return res
|
||||
|
||||
if jikimo_bom.options:
|
||||
res['assembly_options'] = jikimo_bom.options
|
||||
# some_field_value = self.env.context.get('some_field')
|
||||
# if some_field_value == 'condition_value':
|
||||
# res['default_option'] = 'option2' # 设置为特定选项
|
||||
return res
|
||||
def submit(self):
|
||||
if not self.bom_id:
|
||||
raise UserError('缺少bom信息')
|
||||
|
||||
@@ -931,13 +931,6 @@ class SfStockMoveLine(models.Model):
|
||||
if not record.destination_location_id.product_id:
|
||||
record.destination_location_id.product_id = record.product_id.id
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
||||
records = super(SfStockMoveLine, self).create(vals_list)
|
||||
self.put_shelf_location(records)
|
||||
return records
|
||||
|
||||
|
||||
class SfStockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
@@ -1122,6 +1115,12 @@ class SfPickingType(models.Model):
|
||||
'sf_warehouse.group_sf_stock_manager'
|
||||
)
|
||||
|
||||
def _get_action(self, action_xmlid):
|
||||
action = super(SfPickingType, self)._get_action(action_xmlid)
|
||||
if not self.env.user.has_group('base.group_system'):
|
||||
action['context']['create'] = False
|
||||
return action
|
||||
|
||||
|
||||
class CustomStockMove(models.Model):
|
||||
_name = 'stock.move'
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<record model="ir.actions.act_window" id="stock.stock_picking_type_action">
|
||||
<field name="context">{'search_default_groupby_code':1}</field>
|
||||
<field name="domain">[('name', '!=', '制造')]</field>
|
||||
</record>
|
||||
|
||||
<record id="view_location_form_sf_inherit" model="ir.ui.view">
|
||||
|
||||
@@ -77,18 +77,20 @@ class ShelfLocationWizard(models.TransientModel):
|
||||
|
||||
def confirm_the_change(self):
|
||||
if self.destination_barcode_id:
|
||||
stocks = []
|
||||
if self.lot_id:
|
||||
self.current_barcode_id.product_sn_id = False
|
||||
self.destination_barcode_id.product_sn_id = self.lot_id.id
|
||||
self.create_stock_moves(self.lot_id, 1)
|
||||
stocks = self.create_stock_moves(self.lot_id, 1)
|
||||
elif self.current_product_sn_ids:
|
||||
for current_product_sn_id in self.current_product_sn_ids:
|
||||
self.create_stock_moves(current_product_sn_id.lot_id, current_product_sn_id.qty_num)
|
||||
stocks = self.create_stock_moves(current_product_sn_id.lot_id, current_product_sn_id.qty_num)
|
||||
current_product_sn_id.write({
|
||||
'qty_num': 0
|
||||
})
|
||||
else:
|
||||
raise ValidationError('没有需要变更的批次/序列号!')
|
||||
self.env['stock.move.line'].sudo().put_shelf_location(stocks[-1])
|
||||
else:
|
||||
raise ValidationError('请选择目标货位编码!')
|
||||
|
||||
|
||||
138
sg_wechat_enterprise/.gitignore
vendored
138
sg_wechat_enterprise/.gitignore
vendored
@@ -1,138 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
@@ -1,3 +0,0 @@
|
||||
from . import we_api
|
||||
from . import models
|
||||
from . import controllers
|
||||
@@ -1,52 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': '企业微信',
|
||||
'version': '0.1',
|
||||
'summary': '企业通讯录\消息处理\企业应用\无缝登录',
|
||||
'sequence': 30,
|
||||
"author": 'SmartGo Studio.,',
|
||||
'description': '''用于企业内部员工的管理,
|
||||
ER企业微信模块
|
||||
=====================================================
|
||||
主要针对odoo使用微信进行管理,包括以下功能:
|
||||
1) 公众号信息管理(企业号下多applicaiton管理)
|
||||
2) 接收消息处理
|
||||
3) 发送消息处理
|
||||
4) 自定义菜单处理
|
||||
|
||||
....
|
||||
本安装包使用了WechatEnterpriseSDK/wechat_sdk.py,在此表示感谢。
|
||||
源代码可以访问github.地址如下:https://github.com/facert/WechatEnterpriseSDK
|
||||
|
||||
''',
|
||||
'category': '基础信息',
|
||||
'website': 'https://www.smartgo.cn',
|
||||
'depends': ['base', 'mail','hr'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/we_config_view.xml',
|
||||
'views/we_app_view.xml',
|
||||
'views/we_send_message_view.xml',
|
||||
'views/we_receive_message_view.xml',
|
||||
'views/we_message_process_view.xml',
|
||||
'views/we_templates.xml',
|
||||
# "views/mail_view.xml",
|
||||
"views/res_users_view.xml",
|
||||
'views/menu_view.xml',
|
||||
# 'views/we_menu.xml',
|
||||
"data/data.xml"
|
||||
],
|
||||
'demo': [
|
||||
'demo/we_config_demo.xml',
|
||||
],
|
||||
'qweb': [
|
||||
# "static/src/xml/base.xml",
|
||||
# "static/src/xml/account_payment.xml",
|
||||
# "static/src/xml/account_report_backend.xml",
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
# 'post_init_hook': '_auto_install_l10n',
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding:utf-8 -*-
|
||||
|
||||
""" 对企业微信发送给企业后台的消息加解密示例代码.
|
||||
@copyright: Copyright (c) 1998-2014 Tencent Inc.
|
||||
|
||||
"""
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
import base64
|
||||
import string
|
||||
import random
|
||||
import hashlib
|
||||
import time
|
||||
import struct
|
||||
from Crypto.Cipher import AES
|
||||
import xml.etree.cElementTree as ET
|
||||
import socket
|
||||
from . import ierror
|
||||
|
||||
"""
|
||||
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
|
||||
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
|
||||
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
|
||||
"""
|
||||
|
||||
|
||||
class FormatException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def throw_exception(message, exception_class=FormatException):
|
||||
"""my define raise exception function"""
|
||||
raise exception_class(message)
|
||||
|
||||
|
||||
class SHA1:
|
||||
"""计算企业微信的消息签名接口"""
|
||||
|
||||
def getSHA1(self, token, timestamp, nonce, encrypt):
|
||||
"""用SHA1算法生成安全签名
|
||||
@param token: 票据
|
||||
@param timestamp: 时间戳
|
||||
@param encrypt: 密文
|
||||
@param nonce: 随机字符串
|
||||
@return: 安全签名
|
||||
"""
|
||||
try:
|
||||
sortlist = [token, timestamp, nonce, encrypt]
|
||||
sortlist.sort()
|
||||
sha = hashlib.sha1()
|
||||
sort_str = "".join(sortlist)
|
||||
sha.update(sort_str.encode('utf-8'))
|
||||
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
|
||||
|
||||
|
||||
class XMLParse:
|
||||
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
|
||||
|
||||
# xml消息模板
|
||||
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
|
||||
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
|
||||
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
|
||||
<TimeStamp>%(timestamp)s</TimeStamp>
|
||||
<Nonce><![CDATA[%(nonce)s]]></Nonce>
|
||||
</xml>"""
|
||||
|
||||
def extract(self, xmltext):
|
||||
"""提取出xml数据包中的加密消息
|
||||
@param xmltext: 待提取的xml字符串
|
||||
@return: 提取出的加密消息字符串
|
||||
"""
|
||||
try:
|
||||
xml_tree = ET.fromstring(xmltext)
|
||||
encrypt = xml_tree.find("Encrypt")
|
||||
touser_name = xml_tree.find("ToUserName")
|
||||
return ierror.WXBizMsgCrypt_OK, encrypt.text, touser_name.text
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_ParseXml_Error, None, None
|
||||
|
||||
def generate(self, encrypt, signature, timestamp, nonce):
|
||||
"""生成xml消息
|
||||
@param encrypt: 加密后的消息密文
|
||||
@param signature: 安全签名
|
||||
@param timestamp: 时间戳
|
||||
@param nonce: 随机字符串
|
||||
@return: 生成的xml字符串
|
||||
"""
|
||||
resp_dict = {
|
||||
'msg_encrypt': encrypt,
|
||||
'msg_signaturet': signature,
|
||||
'timestamp': timestamp,
|
||||
'nonce': nonce,
|
||||
}
|
||||
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
|
||||
return resp_xml
|
||||
|
||||
|
||||
class PKCS7Encoder():
|
||||
"""提供基于PKCS7算法的加解密接口"""
|
||||
|
||||
block_size = 32
|
||||
|
||||
def encode(self, text):
|
||||
""" 对需要加密的明文进行填充补位
|
||||
@param text: 需要进行填充补位操作的明文
|
||||
@return: 补齐明文字符串
|
||||
"""
|
||||
text_length = len(text)
|
||||
# 计算需要填充的位数
|
||||
amount_to_pad = self.block_size - (text_length % self.block_size)
|
||||
if amount_to_pad == 0:
|
||||
amount_to_pad = self.block_size
|
||||
# 获得补位所用的字符
|
||||
pad = chr(amount_to_pad)
|
||||
return text + pad * amount_to_pad
|
||||
|
||||
def decode(self, decrypted):
|
||||
"""删除解密后明文的补位字符
|
||||
@param decrypted: 解密后的明文
|
||||
@return: 删除补位字符后的明文
|
||||
"""
|
||||
pad = ord(decrypted[-1])
|
||||
if pad < 1 or pad > 32:
|
||||
pad = 0
|
||||
return decrypted[:-pad]
|
||||
|
||||
|
||||
class Prpcrypt(object):
|
||||
"""提供接收和推送给企业微信消息的加解密接口"""
|
||||
|
||||
def __init__(self, key):
|
||||
|
||||
# self.key = base64.b64decode(key+"=")
|
||||
self.key = key
|
||||
# 设置加解密模式为AES的CBC模式
|
||||
self.mode = AES.MODE_CBC
|
||||
|
||||
def encrypt(self, text, corpid):
|
||||
"""对明文进行加密
|
||||
@param text: 需要加密的明文
|
||||
@return: 加密得到的字符串
|
||||
"""
|
||||
# 16位随机字符串添加到明文开头
|
||||
text = self.get_random_str() + str(struct.pack("I", socket.htonl(len(text))), encoding='utf8')\
|
||||
+ str(text, encoding='utf8') + corpid
|
||||
# 使用自定义的填充方式对明文进行补位填充
|
||||
pkcs7 = PKCS7Encoder()
|
||||
text = pkcs7.encode(text)
|
||||
# 加密
|
||||
cryptor = AES.new(self.key, self.mode, self.key[:16])
|
||||
try:
|
||||
ciphertext = cryptor.encrypt(text)
|
||||
# 使用BASE64对加密后的字符串进行编码
|
||||
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
|
||||
|
||||
def decrypt(self, text, corpid):
|
||||
"""对解密后的明文进行补位删除
|
||||
@param text: 密文
|
||||
@return: 删除填充补位后的明文
|
||||
"""
|
||||
try:
|
||||
cryptor = AES.new(self.key, self.mode, self.key[:16])
|
||||
# 使用BASE64对密文进行解码,然后AES-CBC解密
|
||||
plain_text = cryptor.decrypt(base64.b64decode(text))
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_DecryptAES_Error, None
|
||||
try:
|
||||
pad = plain_text[-1]
|
||||
# 去掉补位字符串
|
||||
# pkcs7 = PKCS7Encoder()
|
||||
# plain_text = pkcs7.encode(plain_text)
|
||||
# 去除16位随机字符串
|
||||
content = plain_text[16:-pad]
|
||||
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
|
||||
xml_content = content[4: xml_len + 4]
|
||||
from_corpid = content[xml_len + 4:]
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_IllegalBuffer, None
|
||||
if str(from_corpid, encoding="utf8") != corpid and len(ET.fromstring(xml_content).findall("SuiteId")) < 1:
|
||||
return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None
|
||||
return 0, xml_content
|
||||
|
||||
def get_random_str(self):
|
||||
""" 随机生成16位字符串
|
||||
@return: 16位字符串
|
||||
"""
|
||||
rule = string.ascii_letters + string.digits
|
||||
str = random.sample(rule, 16)
|
||||
return "".join(str)
|
||||
|
||||
|
||||
class WXBizMsgCrypt(object):
|
||||
# 构造函数
|
||||
# @param sToken: 企业微信后台,开发者设置的Token
|
||||
# @param sEncodingAESKey: 企业微信后台,开发者设置的EncodingAESKey
|
||||
# @param sCorpId: 企业号的CorpId
|
||||
def __init__(self, sToken, sEncodingAESKey, sCorpId):
|
||||
try:
|
||||
self.key = base64.b64decode(sEncodingAESKey + "=")
|
||||
assert len(self.key) == 32
|
||||
except:
|
||||
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
|
||||
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
|
||||
self.m_sToken = sToken
|
||||
self.m_sCorpid = sCorpId
|
||||
|
||||
# 验证URL
|
||||
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
|
||||
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
|
||||
# @param sNonce: 随机串,对应URL参数的nonce
|
||||
# @param sEchoStr: 随机串,对应URL参数的echostr
|
||||
# @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
|
||||
# @return:成功0,失败返回对应的错误码
|
||||
|
||||
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
|
||||
sha1 = SHA1()
|
||||
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
if not signature == sMsgSignature:
|
||||
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
|
||||
pc = Prpcrypt(self.key)
|
||||
ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sCorpid)
|
||||
return ret, sReplyEchoStr
|
||||
|
||||
def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
|
||||
# 将企业回复用户的消息加密打包
|
||||
# @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
|
||||
# @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
|
||||
# @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
|
||||
# sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
|
||||
# return:成功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
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
from . import wechat_enterprise
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#########################################################################
|
||||
# Author: jonyqin
|
||||
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
|
||||
# File Name: ierror.py
|
||||
# Description:定义错误码含义
|
||||
#########################################################################
|
||||
WXBizMsgCrypt_OK = 0
|
||||
WXBizMsgCrypt_ValidateSignature_Error = -40001
|
||||
WXBizMsgCrypt_ParseXml_Error = -40002
|
||||
WXBizMsgCrypt_ComputeSignature_Error = -40003
|
||||
WXBizMsgCrypt_IllegalAesKey = -40004
|
||||
WXBizMsgCrypt_ValidateCorpid_Error = -40005
|
||||
WXBizMsgCrypt_EncryptAES_Error = -40006
|
||||
WXBizMsgCrypt_DecryptAES_Error = -40007
|
||||
WXBizMsgCrypt_IllegalBuffer = -40008
|
||||
WXBizMsgCrypt_EncodeBase64_Error = -40009
|
||||
WXBizMsgCrypt_DecodeBase64_Error = -40010
|
||||
WXBizMsgCrypt_GenReturnXml_Error = -40011
|
||||
@@ -1,231 +0,0 @@
|
||||
import time
|
||||
import functools
|
||||
import base64
|
||||
from json import *
|
||||
|
||||
from odoo import http, fields
|
||||
from odoo.http import request
|
||||
from . import WXBizMsgCrypt
|
||||
from werkzeug.exceptions import abort
|
||||
import xml.etree.cElementTree as Et
|
||||
import requests as req
|
||||
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def wechat_login(func):
|
||||
"""
|
||||
用来根据userid取得合作伙伴的id
|
||||
:return:
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kw):
|
||||
# now_time = time.time()
|
||||
# request.session['session_time'] = time.time()
|
||||
# if 'session_time' not in request.session:
|
||||
# request.session['session_time'] = 1
|
||||
# if now_time > request.session['session_time'] + 350:
|
||||
_logger.info(u"没进来之前的kw值为%s" % JSONEncoder().encode(kw))
|
||||
# if not request.session['login'] or not request.session['login'] or request.session[
|
||||
# 'login'] == "public":
|
||||
enterprise, agent_id = request.env['we.config'].sudo().get_odoo_wechat()
|
||||
if enterprise and agent_id:
|
||||
if 'code' in kw and 'state' in kw: # 检查是否取得了code
|
||||
if 'kw' in request.session.keys():
|
||||
_logger.info('code:%s' % kw['code'])
|
||||
account = enterprise.oauth.get_user_info(code=kw['code'])
|
||||
_logger.info('account:%s' % account)
|
||||
if account: # 是否取得了微信企业号通讯录的账号
|
||||
user_detail = enterprise.user.get_detail(account['user_ticket'])
|
||||
_logger.info('user_detail:%s' % user_detail)
|
||||
request.env['we.employee'].we_privacy_update(user_detail)
|
||||
user = request.env['res.users'].sudo().search(
|
||||
[('we_employee_id', '=', account['UserId'])])
|
||||
_logger.info('user:%s' % user)
|
||||
if user: # 是否取得了用户
|
||||
if 'state' in kw:
|
||||
state = base64.b64encode(kw['state'].encode('utf-8')).decode()
|
||||
kw['state'] = state
|
||||
uid = request.session.authenticate(request.session.db, user.login,
|
||||
account['UserId'])
|
||||
kw['user_id'] = uid
|
||||
request.session['session_time'] = time.time()
|
||||
request.session['login'] = user.login
|
||||
|
||||
_logger.info(u"进来之后的kw值为%s" % kw)
|
||||
return func(*args, **kw)
|
||||
else:
|
||||
_logger.warning(u'用户不存在.')
|
||||
return request.render('sg_wechat_enterprise.wechat_warning',
|
||||
{'title': u'警告', 'content': u'该员工的未配置登录用户.'})
|
||||
else:
|
||||
_logger.warning(u'微信企业号验证失败.')
|
||||
return request.render('sg_wechat_enterprise.wechat_warning',
|
||||
{'title': u'警告', 'content': u'微信企业号验证失败.'})
|
||||
else:
|
||||
# 返回时候进入的地方
|
||||
del kw['code']
|
||||
del kw['state']
|
||||
request.session['kw'] = base64.b64encode(JSONEncoder().encode(kw).encode('utf-8')).decode()
|
||||
if len(kw) == 0:
|
||||
base_url = request.httprequest.base_url
|
||||
else:
|
||||
base_url = request.httprequest.base_url + '?'
|
||||
for item, value in kw.items():
|
||||
base_url += item + '=' + value + "&"
|
||||
base_url = base_url[: -1]
|
||||
|
||||
url = enterprise.oauth.authorize_url(base_url,
|
||||
state=base64.b64encode(
|
||||
JSONEncoder().encode(kw).encode('utf-8')).decode(),
|
||||
agent_id=agent_id,
|
||||
scope='snsapi_privateinfo')
|
||||
_logger.warning(u"这是授权的url:" + url)
|
||||
value = {"url": url}
|
||||
return request.render("sg_wechat_enterprise.Transfer", value)
|
||||
else: # 开始微信企业号登录认证
|
||||
request.session['kw'] = base64.b64encode(JSONEncoder().encode(kw).encode('utf-8')).decode()
|
||||
|
||||
if len(kw) == 0:
|
||||
base_url = request.httprequest.base_url
|
||||
else:
|
||||
base_url = request.httprequest.base_url + '?'
|
||||
for item, value in kw.items():
|
||||
base_url += item + '=' + value + "&"
|
||||
base_url = base_url[: -1]
|
||||
url = enterprise.oauth.authorize_url(base_url,
|
||||
state=base64.b64encode(
|
||||
JSONEncoder().encode(kw).encode('utf-8')).decode(),
|
||||
agent_id=agent_id,
|
||||
scope='snsapi_privateinfo'
|
||||
)
|
||||
_logger.warning(u"这是授权的url:" + url)
|
||||
value = {"url": url}
|
||||
return request.render("sg_wechat_enterprise.Transfer", value)
|
||||
else:
|
||||
_logger.warning(u'微信企业号初始化失败.')
|
||||
return request.render('sg_wechat_enterprise.wechat_warning',
|
||||
{'title': u'警告', 'content': u'微信企业号初始化失败.'})
|
||||
|
||||
# return func(*args, **kw)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class WechatEnterprise(http.Controller):
|
||||
"""
|
||||
用于接收微信发过来的任何消息,并转发给相应的业务类进行处理
|
||||
"""
|
||||
__check_str = 'NDOEHNDSY#$_@$JFDK:Q{!'
|
||||
BASE_URL = '/we'
|
||||
|
||||
@wechat_login
|
||||
@http.route(BASE_URL + '/auth', type='http', auth='none')
|
||||
def auth(self, *args, **kw):
|
||||
"""
|
||||
企业微信免登认证
|
||||
"""
|
||||
try:
|
||||
# user_id = (request.session['uid'])
|
||||
redirect1 = kw['redirect'] if 'redirect' in kw else None
|
||||
uid = kw['user_id'] if 'redirect' in kw else None
|
||||
_logger.info('user_id %s', uid)
|
||||
if uid is not False:
|
||||
request.params['login_success'] = True
|
||||
if not redirect1:
|
||||
redirect1 = '/web'
|
||||
redirect1 = redirect1.replace('-', '&').replace('?', '#')
|
||||
logging.info('url:%s' % redirect1)
|
||||
return request.redirect(redirect1)
|
||||
except Exception as ex:
|
||||
_logger.error('无有效的登录凭证.')
|
||||
_logger.warning('auth exceptions:%s' % ex)
|
||||
return request.render('sg_wechat_enterprise.wechat_warning',
|
||||
{'title': u'警告', 'content': u'无有效的登录凭证.'})
|
||||
|
||||
@http.route('/WechatEnterprise/<string:code>/api', type='http', auth="public", methods=["GET", "POST"], csrf=False)
|
||||
def process(self, code, **kwargs):
|
||||
"""
|
||||
处理从微信服务器发送过来的请求
|
||||
:param code: 自定义代码
|
||||
:param kwargs: 包含 (msg_signature, timestamp, nonce, echostr) 等参数
|
||||
:return:
|
||||
"""
|
||||
_logger.info(u'处理从微信服务器发送过来的请求code: %s, kwargs: %s' % (code, kwargs))
|
||||
app_id = request.env['we.app'].sudo().search([('code', '=', code)], limit=1)
|
||||
if not app_id:
|
||||
_logger.warning(u'Can not find wechat app by code: {code}')
|
||||
abort(403)
|
||||
corp_id = app_id.enterprise_id
|
||||
we_chat_cpt = WXBizMsgCrypt.WXBizMsgCrypt(app_id.Token, app_id.EncodingAESKey, corp_id.corp_id)
|
||||
signature, timestamp, nonce = kwargs['msg_signature'], kwargs['timestamp'], kwargs['nonce']
|
||||
if kwargs.get('echostr'):
|
||||
echo_string = kwargs['echostr']
|
||||
sort_list = [app_id.Token, timestamp, nonce, echo_string]
|
||||
if request.env['we.tools'].sudo(). \
|
||||
check_message_signature(message_list=sort_list, msg_signature=signature):
|
||||
ret, signature_echo_string = we_chat_cpt.VerifyURL(signature, timestamp, nonce, echo_string)
|
||||
if ret == 0:
|
||||
return str(signature_echo_string, encoding="utf8")
|
||||
body_text = request.httprequest.data
|
||||
ret, signature_message = we_chat_cpt.DecryptMsg(body_text, signature, timestamp, nonce)
|
||||
xml_tree = Et.fromstring(signature_message)
|
||||
if len(xml_tree.findall("SuiteId")) > 0:
|
||||
return "success"
|
||||
if len(xml_tree.find("ApprovalInfo")) > 0:
|
||||
xmlstr = etree.fromstring(signature_message)
|
||||
# data = xml2json_from_elementtree(xmlstr)
|
||||
return request.env['we.receive.message'].sudo().sys_approval_change(xml_tree.find("ApprovalInfo"))
|
||||
data = {
|
||||
'MsgType': xml_tree.find("MsgType").text,
|
||||
# 'AgentID': xml_tree.find("AgentID").text,
|
||||
'ToUserName': xml_tree.find("ToUserName").text,
|
||||
'FromUserName': xml_tree.find("FromUserName").text,
|
||||
'CreateTime': xml_tree.find("CreateTime").text
|
||||
}
|
||||
if xml_tree.find("AgentID") != None:
|
||||
data['AgentID'] = xml_tree.find("AgentID").text
|
||||
if data["MsgType"] == "text":
|
||||
data["Content"] = xml_tree.find("Content").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "image":
|
||||
data["PicUrl"] = xml_tree.find("PicUrl").text
|
||||
data["MediaId"] = xml_tree.find("MediaId").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "voice":
|
||||
data["MediaId"] = xml_tree.find("MediaId").text
|
||||
data["Format"] = xml_tree.find("Format").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "video" or data["MsgType"] == "shortvideo":
|
||||
data["MediaId"] = xml_tree.find("MediaId").text
|
||||
data["ThumbMediaId"] = xml_tree.find("ThumbMediaId").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "location":
|
||||
data["Location_X"] = xml_tree.find("Location_X").text
|
||||
data["Location_Y"] = xml_tree.find("Location_Y").text
|
||||
data["Scale"] = xml_tree.find("Scale").text
|
||||
data["Label"] = xml_tree.find("Label").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "link":
|
||||
data["Title"] = xml_tree.find("Title").text
|
||||
data["Description"] = xml_tree.find("Description").text
|
||||
data["PicUrl"] = xml_tree.find("PicUrl").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "event":
|
||||
if xml_tree.find("Event") == "subscribe" or xml_tree.find("Event") == "unsubscribe":
|
||||
data["Event"] = xml_tree.find("Event").text
|
||||
else:
|
||||
ret, signature_message = we_chat_cpt.EncryptMsg(signature_message, nonce, timestamp)
|
||||
return signature_message
|
||||
request.env['we.receive.message'].sudo().process_message(data)
|
||||
return ''
|
||||
|
||||
@http.route('/WechatEnterprise/transfer', type='http', auth="public", methods=["POST", "GET"], csrf=False)
|
||||
def transfer(self, url):
|
||||
value = {"url": url}
|
||||
return request.render('sg_wechat_enterprise.Transfer', value)
|
||||
@@ -1,16 +0,0 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.cron" id="ir_cron_dingtalk_accesstoken">
|
||||
<field name="name">SNS Message To 企业微信</field>
|
||||
<field name="model_id" ref="mail.model_mail_message"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.send_we_message()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="main_wechat_enterprise" model="we.config">
|
||||
<field name="name">SmartGo</field>
|
||||
<field name="corp_id">wwad4f2c227d490637</field>
|
||||
<field name="corp_secret">kq_AzJN1FoPdWjyEwAQs_cqzJhALmKhmwYMBQyJzuEs</field>
|
||||
<field name="company_id">1</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from . import we_app
|
||||
from . import we_conf
|
||||
from . import we_send_message
|
||||
from . import we_receive_message
|
||||
from . import we_receive_message_process
|
||||
from . import res_users
|
||||
from . import we_tools
|
||||
from . import mail
|
||||
from . import client
|
||||
@@ -1,11 +0,0 @@
|
||||
from wechatpy.enterprise import WeChatClient
|
||||
|
||||
from . import api
|
||||
|
||||
|
||||
class JkmWeChatClient(WeChatClient):
|
||||
def __init__(self, corp_id, secret, session=None):
|
||||
super(JkmWeChatClient, self).__init__(corp_id, secret, session=session)
|
||||
|
||||
oauth = api.JkmWechatOauth()
|
||||
user = api.JkmWechatUser()
|
||||
@@ -1,2 +0,0 @@
|
||||
from .jkm_user import JkmWechatUser
|
||||
from .jkm_oauth import JkmWechatOauth
|
||||
@@ -1,53 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import six
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class JkmWechatOauth(BaseWeChatAPI):
|
||||
OAUTH_BASE_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize'
|
||||
|
||||
def authorize_url(self, redirect_uri, state=None, agent_id=None, scope='snsapi_base'):
|
||||
"""
|
||||
构造网页授权链接
|
||||
详情请参考
|
||||
https://work.weixin.qq.com/api/doc#90000/90135/91022
|
||||
|
||||
:param redirect_uri: 授权后重定向的回调链接地址
|
||||
:param state: 重定向后会带上 state 参数
|
||||
:param agent_id: 企业应用的id
|
||||
:param scope: 应用授权作用域
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
redirect_uri = six.moves.urllib.parse.quote(redirect_uri, safe=b'')
|
||||
url_list = [
|
||||
self.OAUTH_BASE_URL,
|
||||
'?appid=',
|
||||
self._client.corp_id,
|
||||
'&redirect_uri=',
|
||||
redirect_uri,
|
||||
'&response_type=code&scope=',
|
||||
scope,
|
||||
]
|
||||
if state:
|
||||
url_list.extend(['&state=', state])
|
||||
url_list.append('#wechat_redirect')
|
||||
if agent_id:
|
||||
url_list.extend(['&agentid=', str(agent_id)])
|
||||
return ''.join(url_list)
|
||||
|
||||
def get_user_info(self, code):
|
||||
"""
|
||||
获取访问用户身份
|
||||
详情请参考
|
||||
https://work.weixin.qq.com/api/doc#90000/90135/91023
|
||||
|
||||
:param code: 通过成员授权获取到的code
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
|
||||
return self._get(
|
||||
'user/getuserinfo',
|
||||
params={
|
||||
'code': code,
|
||||
}
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
from wechatpy.enterprise.client.api import WeChatUser
|
||||
|
||||
|
||||
class JkmWechatUser(WeChatUser):
|
||||
|
||||
def get_detail(self, user_ticket):
|
||||
"""
|
||||
获取访问用户敏感信息
|
||||
详情请参考
|
||||
https://developer.work.weixin.qq.com/document/path/95833
|
||||
|
||||
:param user_ticket: 成员票据
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'user/getuserdetail',
|
||||
data={
|
||||
'user_ticket': user_ticket
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# -*- coding: utf-8-*-
|
||||
import re
|
||||
##过滤HTML中的标签
|
||||
#将HTML中标签等信息去掉
|
||||
#@param htmlstr HTML字符串.
|
||||
def filter_tags(htmlstr):
|
||||
#先过滤CDATA
|
||||
re_cdata=re.compile('//<!\[CDATA\[[^>]*//\]\]>',re.I) #匹配CDATA
|
||||
re_script=re.compile('<\s*script[^>]*>[^<]*<\s*/\s*script\s*>',re.I)#Script
|
||||
re_style=re.compile('<\s*style[^>]*>[^<]*<\s*/\s*style\s*>',re.I)#style
|
||||
re_br=re.compile('<br\s*?/?>')#处理换行
|
||||
re_h=re.compile('</?\w+[^>]*>')#HTML标签
|
||||
re_comment=re.compile('<!--[^>]*-->')#HTML注释
|
||||
s=re_cdata.sub('',htmlstr)#去掉CDATA
|
||||
s=re_script.sub('',s) #去掉SCRIPT
|
||||
s=re_style.sub('',s)#去掉style
|
||||
s=re_br.sub('\n',s)#将br转换为换行
|
||||
s=re_h.sub('',s) #去掉HTML 标签
|
||||
s=re_comment.sub('',s)#去掉HTML注释
|
||||
#去掉多余的空行
|
||||
blank_line=re.compile('\n+')
|
||||
s=blank_line.sub('\n',s)
|
||||
s=replaceCharEntity(s)#替换实体
|
||||
return s
|
||||
|
||||
##替换常用HTML字符实体.
|
||||
#使用正常的字符替换HTML中特殊的字符实体.
|
||||
#你可以添加新的实体字符到CHAR_ENTITIES中,处理更多HTML字符实体.
|
||||
#@param htmlstr HTML字符串.
|
||||
def replaceCharEntity(htmlstr):
|
||||
CHAR_ENTITIES={'nbsp':' ','160':' ',
|
||||
'lt':'<','60':'<',
|
||||
'gt':'>','62':'>',
|
||||
'amp':'&','38':'&',
|
||||
'quot':'"','34':'"',}
|
||||
|
||||
re_charEntity=re.compile(r'&#?(?P<name>\w+);')
|
||||
sz=re_charEntity.search(htmlstr)
|
||||
while sz:
|
||||
entity=sz.group()#entity全称,如>
|
||||
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
|
||||
@@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import random
|
||||
from odoo import models, fields, api
|
||||
from odoo.http import request
|
||||
from odoo.exceptions import AccessDenied
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
we_employee_id = fields.Char(string=u'企业微信账号', default="")
|
||||
|
||||
def _check_credentials(self, we_employee_id, env):
|
||||
"""
|
||||
用户验证
|
||||
"""
|
||||
try:
|
||||
return super(ResUsers, self)._check_credentials(we_employee_id, env)
|
||||
except AccessDenied:
|
||||
user_id = self.env['res.users'].sudo().search([('we_employee_id', '=', we_employee_id)])
|
||||
if not (user_id and user_id.id == self.env.uid):
|
||||
raise
|
||||
@@ -1,213 +0,0 @@
|
||||
from odoo import api, models, exceptions, fields
|
||||
import logging
|
||||
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseApp(models.Model):
|
||||
_name = 'we.app'
|
||||
_description = 'Wechat Enterprise App Manage'
|
||||
|
||||
agentid = fields.Integer(string=u'应用ID', required=True)
|
||||
code = fields.Char(string=u'自定义代码')
|
||||
name = fields.Char(u'企业号应用名称')
|
||||
description = fields.Text(u'企业号应用详情')
|
||||
redirect_domain = fields.Char(u'企业应用可信域名')
|
||||
isreportuser = fields.Selection([('0', u'不接受'), ('1', u'接收')], u'变更通知', required=True, default='0')
|
||||
isreportenter = fields.Selection([('0', u'不接受'),
|
||||
('1', u'接收')], u'进入应用事件上报', required=True, default='0')
|
||||
|
||||
enterprise_id = fields.Many2one('we.config', string=u'企业微信')
|
||||
|
||||
secret = fields.Char(string=u'Secret', size=100)
|
||||
home_url = fields.Char(u'主页型应用url')
|
||||
square_logo_url = fields.Char(u'方形头像url')
|
||||
round_logo_url = fields.Char(u'圆形头像url')
|
||||
type = fields.Selection([('1', u'消息型'), ('2', u'主页型')], u'应用类型', required=True, default='1')
|
||||
allow_userinfos = fields.Char(u'企业应用可见范围(人员)')
|
||||
allow_partys = fields.Char(u'企业应用可见范围(部门)')
|
||||
allow_tags = fields.Char(u'企业应用可见范围(标签)')
|
||||
report_location_flag = fields.Selection([('0', u'不上报'), ('1', u'进入会话上报'), ('2', u'持续上报')], u'位置上报',
|
||||
required=True, default='1')
|
||||
logo_mediaid = fields.Char(u'企业应用头像的mediaid')
|
||||
close = fields.Selection([('0', u'否'), ('1', u'是')], u'是否禁用', required=True, default='0')
|
||||
app_menu_ids = fields.One2many('we.app.menu', 'agentid', string=u'自定义菜单')
|
||||
Token = fields.Char(u'Token')
|
||||
EncodingAESKey = fields.Char(u'EncodingAESKey')
|
||||
|
||||
def pull_app_from_we(self, wechat):
|
||||
"""
|
||||
从微信获取app列表
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
app_lists = wechat.agent.list()
|
||||
if app_lists:
|
||||
for app_list in app_lists:
|
||||
if 'agentid' in app_list:
|
||||
app_detail = wechat.agent.get(app_list['agentid'])
|
||||
if app_detail:
|
||||
data = {
|
||||
'agentid': str(app_detail['agentid'])
|
||||
}
|
||||
my_app = request.env["we.app"].search(
|
||||
[("agentid", "=", str(app_detail['agentid']))])
|
||||
if my_app and len(my_app) > 0:
|
||||
continue
|
||||
data['name'] = app_detail['name']
|
||||
data['square_logo_url'] = app_detail['square_logo_url']
|
||||
data['description'] = app_detail['description']
|
||||
data['close'] = str(app_detail['close'])
|
||||
data['redirect_domain'] = app_detail['redirect_domain']
|
||||
data['report_location_flag'] = str(app_detail['report_location_flag'])
|
||||
data['isreportenter'] = str(app_detail['isreportenter'])
|
||||
data['enterprise_id'] = self.id
|
||||
request.env["we.app"].create(data)
|
||||
|
||||
except Exception as e:
|
||||
raise Warning((u'获取应用列表失败,原因:%s', str(e)))
|
||||
|
||||
def push_app_to_we(self):
|
||||
"""
|
||||
同步app到微信
|
||||
:return:
|
||||
"""
|
||||
wechat = self.env['we.config'].sudo().get_wechat()
|
||||
if wechat:
|
||||
try:
|
||||
for account in self:
|
||||
app_json = {
|
||||
'name': account.name
|
||||
}
|
||||
if account.description:
|
||||
app_json['description'] = account.description
|
||||
if account.redirect_domain:
|
||||
app_json['redirect_domain'] = account.redirect_domain
|
||||
app_json['agentid'] = int(account.agentid)
|
||||
app_json['report_location_flag'] = int(account.report_location_flag)
|
||||
if account.type == "1": # 消息型应用
|
||||
if account.name and account.agentid \
|
||||
and account.isreportuser and account.isreportenter and account.report_location_flag:
|
||||
app_json['isreportuser'] = int(account.isreportuser)
|
||||
app_json['isreportenter'] = int(account.isreportenter)
|
||||
wechat.agent.set(agent_id=app_json['agentid'], name=app_json['name'],
|
||||
description=app_json['description'],
|
||||
redirect_domain=app_json['redirect_domain'],
|
||||
is_report_user=app_json['isreportuser'],
|
||||
is_report_enter=app_json['isreportenter'],
|
||||
report_location_flag=app_json['report_location_flag'])
|
||||
elif account.type == "2": # 主页型应用
|
||||
if account.name and account.agentid \
|
||||
and account.report_location_flag and account.home_url:
|
||||
app_json['home_url'] = account.home_url
|
||||
wechat.agent.set(agent_id=app_json['agentid'], name=app_json['name'],
|
||||
description=app_json['description'],
|
||||
redirect_domain=app_json['redirect_domain'],
|
||||
is_report_user=app_json['isreportuser'],
|
||||
is_report_enter=app_json['isreportenter'],
|
||||
report_location_flag=app_json['report_location_flag'])
|
||||
|
||||
except Exception as e:
|
||||
_logger.warning(u'更新app失败,原因:%s', str(e))
|
||||
raise Warning(u'更新app失败,原因:%s', str(e))
|
||||
else:
|
||||
raise exceptions.Warning(u"初始化企业号失败")
|
||||
|
||||
def update_app_menu(self):
|
||||
"""
|
||||
同步菜单至app
|
||||
:return:
|
||||
"""
|
||||
wechat = self.env['we.config'].sudo().get_wechat(agentID=self.agentid)
|
||||
menus = self.env['we.app.menu'].sudo().search([("agentid", "=", self.name)])
|
||||
wechat.menu.delete(agent_id=self.agentid)
|
||||
menu_json = {'button': []}
|
||||
button = []
|
||||
if wechat and menus:
|
||||
for menu in menus:
|
||||
menu_data = {
|
||||
'name': menu['name']
|
||||
}
|
||||
if not menu['partner_menu_id']:
|
||||
sub_menus = request.env['we.app.menu'].sudo().search(
|
||||
[("agentid", "=", self.name), ("partner_menu_id", "=", menu['name'])])
|
||||
if sub_menus and (len(sub_menus) > 0) and (len(sub_menus) < 6):
|
||||
sub_menu_list = []
|
||||
for sub_menu in sub_menus:
|
||||
sub_menu_data = {
|
||||
'name': sub_menu['name']
|
||||
}
|
||||
if menu['type'] == 'xml' or menu['type'] == 'sub_button':
|
||||
sub_menu_data['type'] = sub_menu['type']
|
||||
sub_menu_data['url'] = sub_menu['url']
|
||||
else:
|
||||
sub_menu_data['type'] = sub_menu['type']
|
||||
sub_menu_data['key'] = sub_menu['key']
|
||||
sub_menu_list.append(sub_menu_data)
|
||||
menu_data['sub_button'] = sub_menu_list
|
||||
else:
|
||||
if menu['type'] == 'xml' or menu['type'] == 'sub_button':
|
||||
menu_data['type'] = menu['type']
|
||||
menu_data['url'] = menu['url']
|
||||
else:
|
||||
menu_data['type'] = menu['type']
|
||||
menu_data['key'] = menu['key']
|
||||
button.append(menu_data)
|
||||
menu_json['button'] = button
|
||||
wechat.menu.update(agent_id=self.agentid, menu_data=menu_json)
|
||||
else:
|
||||
raise exceptions.Warning(u"初始化企业号失败或该应用无菜单")
|
||||
|
||||
|
||||
class WechatEnterpriseAppMenu(models.Model):
|
||||
_name = 'we.app.menu'
|
||||
_description = 'Wechat Enterprise App Menu Manage'
|
||||
|
||||
agentid = fields.Many2one('we.app', u'企业应用', required=True)
|
||||
partner_menu_id = fields.Many2one('we.app.menu', u'上级菜单')
|
||||
type = fields.Selection(
|
||||
[('sub_button', u'跳转至子菜单'), ('click', u'点击推事件'), ('xml', u'跳转URL'), ('scancode_push', u'扫码推事件'),
|
||||
('scancode_waitmsg', u'扫码推事件且弹出“消息接收中”提示框'),
|
||||
('pic_sysphoto', u'弹出系统拍照发图'), ('pic_photo_or_album', u'弹出拍照或者相册发图'), ('pic_weixin', u'弹出微信相册发图器'),
|
||||
('location_select', u'弹出地理位置选择器')], u'按钮的类型', required=True, default='xml')
|
||||
name = fields.Char(u'菜单标题', required=True)
|
||||
key = fields.Char(u'菜单KEY值')
|
||||
url = fields.Char(u'网页链接')
|
||||
|
||||
@api.constrains('partner_menu_id', 'name')
|
||||
def _check_menu_name_length(self):
|
||||
if self.name and self.partner_menu_id and len(self.name) > 7:
|
||||
raise Warning(u'二级菜单显示名称不能超过14个字符或7个汉字.')
|
||||
elif self.name and not self.partner_menu_id and len(self.name) > 4:
|
||||
raise Warning(u'一级菜单显示名称不能超过8个字符或4个汉字.')
|
||||
else:
|
||||
return True
|
||||
|
||||
@api.constrains('agentid')
|
||||
def check_menu_number(self):
|
||||
"""
|
||||
取得一个app的一级菜单量
|
||||
:return:
|
||||
"""
|
||||
menus_ids = self.sudo().search([('agentid', '=', self.agentid['name']), ('partner_menu_id', '=', False)])
|
||||
|
||||
if menus_ids and len(menus_ids) > 3:
|
||||
raise Warning(u'公众号的一级菜单数据不能超过3个.')
|
||||
|
||||
return True
|
||||
|
||||
@api.constrains('partner_menu_id')
|
||||
def check_submenu_number(self):
|
||||
"""
|
||||
取得一个一级菜单的子菜单数量
|
||||
:return:
|
||||
"""
|
||||
sub_menus_ids = self.sudo().search(
|
||||
[('partner_menu_id', '=', self.partner_menu_id['name']), ('partner_menu_id', '!=', False)])
|
||||
|
||||
if sub_menus_ids and len(sub_menus_ids) > 5:
|
||||
raise Warning(u'一级菜单的二级子菜单数据不能超过5个.')
|
||||
|
||||
return True
|
||||
@@ -1,62 +0,0 @@
|
||||
import logging
|
||||
import requests
|
||||
import time
|
||||
import base64
|
||||
from odoo.http import request
|
||||
from odoo import api, models, exceptions, fields, _
|
||||
from wechatpy.enterprise import WeChatClient
|
||||
from .client import JkmWeChatClient
|
||||
from json import *
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseConfigration(models.Model):
|
||||
_name = 'we.config'
|
||||
|
||||
name = fields.Char(u'名称', required=True, help=u'取个好名字吧!')
|
||||
corp_id = fields.Char(u'企业ID', required=True, help=u'企业ID,必填项')
|
||||
corp_secret = fields.Char(u'通讯录同步Secret', required=True, help=u'通讯录同步Secret,必填项')
|
||||
odoo_app_id = fields.Many2one('we.app', u'Odoo应用', help=u'在企业微信工作台配置的与Odoo进行连接的应用')
|
||||
company_id = fields.Many2one('res.company', string=u'所属公司')
|
||||
|
||||
_sql_constraints = [
|
||||
('code_complete_name_uniq', 'unique (company_id)', '一个所属公司只能定义一个企业微信!')
|
||||
]
|
||||
|
||||
def get_wechat(self, agent_id=None, company_id=1):
|
||||
"""
|
||||
取得wechat app的实例
|
||||
:param agent_id: conf or None (是企业号对象还是应用对象)
|
||||
:return:
|
||||
"""
|
||||
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
|
||||
if agent_id:
|
||||
enterprise_app = self.env['we.app'].sudo().search([('agentid', '=', agent_id)])
|
||||
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise_app.secret)
|
||||
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise.corp_secret)
|
||||
|
||||
def get_odoo_wechat(self, company_id=1):
|
||||
"""
|
||||
取得Odoo wechat app的实例
|
||||
:param agent_id: conf or None (是企业号对象还是应用对象)
|
||||
:return:
|
||||
"""
|
||||
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
|
||||
if enterprise.odoo_app_id:
|
||||
return (JkmWeChatClient(corp_id=enterprise.corp_id, secret=enterprise.odoo_app_id.secret),
|
||||
enterprise.odoo_app_id.agentid)
|
||||
else:
|
||||
raise exceptions.Warning(u'Odoo应用未配置. ')
|
||||
|
||||
def get_wechat_app(self, app_code=None, company_id=1):
|
||||
"""
|
||||
取得wechat app的实例
|
||||
:param app_code: 应用代码
|
||||
:return:
|
||||
"""
|
||||
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
|
||||
if app_code:
|
||||
enterprise_app = self.env['we.app'].sudo().search([('code', '=', app_code)])
|
||||
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise_app.secret)
|
||||
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise.corp_secret)
|
||||
@@ -1,143 +0,0 @@
|
||||
from odoo import api, models, fields
|
||||
import logging
|
||||
from odoo.http import request
|
||||
import datetime
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseReceiveMessage(models.Model):
|
||||
_name = 'we.receive.message'
|
||||
_description = 'Wechat Enterprise Receive Message Manage'
|
||||
|
||||
name = fields.Char(required=True, index=True)
|
||||
ToUserName = fields.Char(u'企业号CorpID')
|
||||
FromUserName = fields.Char(u'成员UserID')
|
||||
CreateTime = fields.Char(u'消息创建时间')
|
||||
MsgId = fields.Char(u'消息id')
|
||||
AgentID = fields.Many2one('we.app', u'企业应用')
|
||||
MsgType = fields.Selection([('text', u'文本'),
|
||||
('voice', u'语音'),
|
||||
('image', u'图片'),
|
||||
('video', u'视频'),
|
||||
('shortvideo', u'短视频'),
|
||||
('location', u'位置'),
|
||||
('link', u'链接'),
|
||||
('subscribe', u'关注'),
|
||||
('unsubscribe', u'取消关注'),
|
||||
('xml', u'自定义菜单链接跳转'),
|
||||
('click', u'自定义菜单点击'),
|
||||
('scan', u'扫描二维码'),
|
||||
('scancode_waitmsg', u'扫描二维码并等待'),
|
||||
('event', u'取消关注')],
|
||||
string=u'消息类型', required=True, default='text')
|
||||
Content = fields.Text(u'文本消息内容')
|
||||
state = fields.Selection([('1', u'未处理'), ('2', u'已处理'), ('3', u'处理失败')], u'状态', default='1')
|
||||
PicUrl = fields.Char(u'图片链接')
|
||||
MediaId = fields.Char(u'图片媒体文件id')
|
||||
Format = fields.Char(u'语音格式')
|
||||
ThumbMediaId = fields.Char(u'视频消息缩略图的媒体id')
|
||||
Location_X = fields.Char(u'地理位置纬度')
|
||||
Location_Y = fields.Char(u'地理位置经度')
|
||||
Scale = fields.Char(u'地图缩放大小')
|
||||
Label = fields.Char(u'地理位置信息')
|
||||
Title = fields.Char(u'标题')
|
||||
Description = fields.Char(u'描述')
|
||||
Cover_PicUrl = fields.Char(u'封面缩略图的url')
|
||||
|
||||
@api.depends('MsgType', 'MsgId')
|
||||
def name_get(self):
|
||||
result = []
|
||||
for receive in self:
|
||||
name = receive.MsgType + '_' + receive.MsgId
|
||||
result.append((receive.id, name))
|
||||
return result
|
||||
|
||||
def add_message(self, data):
|
||||
"""
|
||||
增加一条待处理的上传消息
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
|
||||
app = request.env['we.app'].sudo().search([("agentid", "=", data["AgentID"])])
|
||||
receive_message_data = {
|
||||
'AgentID': app.id,
|
||||
'MsgType': data["MsgType"],
|
||||
'FromUserName': data["FromUserName"],
|
||||
'ToUserName': data["ToUserName"]
|
||||
}
|
||||
current_time = datetime.datetime.now()
|
||||
real_time = current_time + datetime.timedelta(hours=8)
|
||||
receive_message_data["CreateTime"] = real_time
|
||||
receive_message_data["name"] = data["MsgType"] + data["MsgId"]
|
||||
|
||||
if data["MsgType"] == "text":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["Content"] = data["Content"]
|
||||
if data["MsgType"] == "image":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["PicUrl"] = data["PicUrl"]
|
||||
receive_message_data["MediaId"] = data["MediaId"]
|
||||
if data["MsgType"] == "voice":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["MediaId"] = data["MediaId"]
|
||||
receive_message_data["Format"] = data["Format"]
|
||||
if data["MsgType"] == "video" or data["MsgType"] == "shortvideo":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["MediaId"] = data["MediaId"]
|
||||
receive_message_data["ThumbMediaId"] = data["ThumbMediaId"]
|
||||
if data["MsgType"] == "location":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["Location_X"] = data["Location_X"]
|
||||
receive_message_data["Location_Y"] = data["Location_Y"]
|
||||
receive_message_data["Scale"] = data["Scale"]
|
||||
receive_message_data["Label"] = data["Label"]
|
||||
if data["MsgType"] == "link":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["Title"] = data["Title"]
|
||||
receive_message_data["Description"] = data["Description"]
|
||||
receive_message_data["Cover_PicUrl"] = data["PicUrl"]
|
||||
if data["MsgType"] == "event":
|
||||
if data["Event"] == "subscribe":
|
||||
receive_message_data["MsgType"] = "subscribe"
|
||||
if data["Event"] == "unsubscribe":
|
||||
receive_message_data["MsgType"] = "unsubscribe"
|
||||
|
||||
return super(WechatEnterpriseReceiveMessage, self).create(receive_message_data)
|
||||
|
||||
def process_message(self, data):
|
||||
"""
|
||||
处理未处理和失败的消息
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
messages = self.sudo().add_message(data)
|
||||
for message in messages:
|
||||
if message:
|
||||
if message.state == '2':
|
||||
break
|
||||
if data["MsgType"] == "text":
|
||||
process = self.env['we.receive.message.process'].get_message_process(data["MsgType"],
|
||||
data["Content"],
|
||||
data["AgentID"])
|
||||
else:
|
||||
process = self.env['we.receive.message.process'].get_message_process(data["MsgType"],
|
||||
" ",
|
||||
data["AgentID"])
|
||||
try:
|
||||
if process:
|
||||
if data["MsgType"] == "voice" or data["MsgType"] == "image" or data["MsgType"] == "video" or \
|
||||
data["MsgType"] == "shortvideo":
|
||||
process.sudo().exec_class_mothed(data["FromUserName"], data["AgentID"], data["MediaId"])
|
||||
else:
|
||||
process.sudo().exec_class_mothed(data["FromUserName"], data["AgentID"])
|
||||
else:
|
||||
return self.env['we.send.message'].sudo().send_text_message(data["FromUserName"],
|
||||
data["AgentID"],
|
||||
content=u'感谢您的关注!')
|
||||
|
||||
message.sudo().write({'state': '1'})
|
||||
except Exception as e:
|
||||
message.sudo().write({'state': u'处理失败'})
|
||||
raise Warning(u'处理失败, 原因:%s', str(e))
|
||||
@@ -1,109 +0,0 @@
|
||||
from odoo import models, fields
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseReceiveMessageProcess(models.Model):
|
||||
_name = 'we.receive.message.process'
|
||||
_description = 'Wechat Enterprise Process Receive Process Message'
|
||||
|
||||
name = fields.Char(u'名称', help=u'取个好名称方便管理,比如优惠信息索取', required=True)
|
||||
message_type = fields.Selection([('text', u'文本'),
|
||||
('voice', u'语音'),
|
||||
('image', u'图片'),
|
||||
('video', u'视频'),
|
||||
('shortvideo', u'短视频'),
|
||||
('location', u'位置'),
|
||||
('link', u'链接'),
|
||||
('subscribe', u'关注'),
|
||||
('unsubscribe', u'取消关注'),
|
||||
('xml', u'自定义菜单链接跳转'),
|
||||
('click', u'自定义菜单点击'),
|
||||
('scan', u'扫描二维码'),
|
||||
('scancode_waitmsg', u'扫描二维码并等待'),
|
||||
('unsubscribe', u'取消关注')],
|
||||
string=u'消息类型', required=True)
|
||||
message_key = fields.Char(u'关键字', required=True)
|
||||
class_name = fields.Char(u'负责处理的类', help='此处填写进行处理的类的名称),例如:topro_service_base.test', required=True, default="类的名称")
|
||||
method_name = fields.Char(u'负责处理的方法', help='此处填写进入处理的方法名,方法必须包括参数是message和account_id(微信公众号的id),这是一个dict',
|
||||
required=True, default="方法名")
|
||||
agentID = fields.Many2one('we.app', u'企业应用', required=True)
|
||||
note = fields.Text(u'备注')
|
||||
|
||||
def get_message_process(self, message_type, key_word=False, agent_id=False):
|
||||
"""
|
||||
取得消息处理的设置
|
||||
:param message_type:
|
||||
:param key_word:
|
||||
:param agent_id:
|
||||
:return:
|
||||
"""
|
||||
process = False
|
||||
if message_type:
|
||||
process = self.sudo().search(
|
||||
[('message_type', '=', message_type), ('message_key', '=', key_word), ('agentID', '=', agent_id)],
|
||||
limit=1)
|
||||
if not process and message_type and key_word:
|
||||
process = self.sudo().search([('message_type', '=', message_type), ('message_key', '=', key_word)], limit=1)
|
||||
if not process and message_type:
|
||||
process = self.sudo().search([('message_type', '=', message_type)], limit=1)
|
||||
return process
|
||||
|
||||
def exec_by_message_type(self, message_type, message_key, agent_id):
|
||||
"""
|
||||
根据消息的类型动态调用类进行执行
|
||||
:param message_type:
|
||||
:param message_key:
|
||||
:param agent_id:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# 取得对象,
|
||||
if message_type and message_key:
|
||||
process = self.get_message_process(message_type, message_key, agent_id)
|
||||
process.sudo().exec_class_mothed(message_key, agent_id)
|
||||
|
||||
def exec_class_mothed(self, from_user_name, agent_id, media_id=None):
|
||||
"""
|
||||
执行类的方法
|
||||
:param from_user_name:
|
||||
:param agent_id:
|
||||
:param media_id:
|
||||
:return:
|
||||
"""
|
||||
|
||||
_logger.debug('exec_class_mothed')
|
||||
object_function = getattr(self.env[self.class_name], self.method_name)
|
||||
ret = object_function(from_user_name, agent_id, media_id)
|
||||
|
||||
return ret
|
||||
|
||||
def hello(self, from_user_name, agent_id):
|
||||
"""
|
||||
demo方法,模拟动态处理客户的需求
|
||||
:param from_user_name:
|
||||
:param agent_id:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
self.env['we.send.message'].sudo(). \
|
||||
send_news_message(from_user_name, agent_id, u'测试图文信息', u'测试图文信息的描述',
|
||||
'http://www.baidu.com',
|
||||
'http://www.kia-hnsyt.com.cn/uploads/allimg/141204/1-1412041624240-L.jpg')
|
||||
except Exception as e:
|
||||
_logger.warning(u'发送微信文本失败原因:%s', str(e))
|
||||
raise Warning(str(e))
|
||||
|
||||
def send_img(self, from_user_name, agent_id):
|
||||
"""
|
||||
demo方法,模拟动态处理客户的需求
|
||||
:param from_user_name:
|
||||
:param agent_id:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
self.env['we.send.message'].sudo().send_text_message(from_user_name, agent_id, u'即将为您发送一条消息')
|
||||
except Exception as e:
|
||||
_logger.warning(u'发送微信文本失败原因:%s', str(e))
|
||||
raise Warning(str(e))
|
||||
@@ -1,108 +0,0 @@
|
||||
from odoo import api, models, exceptions, fields
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseSendMessage(models.Model):
|
||||
_name = 'we.send.message'
|
||||
_description = 'Wechat Enterprise Send Message Manage'
|
||||
|
||||
touser = fields.Many2many('res.users', 'send_message_to_users_ref',
|
||||
'wechat_contacts_id', 'touser', u'成员列表')
|
||||
msgtype = fields.Selection([('text', u'文字消息'), ('image', u'图片消息'), ('voice', u'语音消息'), ('video', u'视频消息'),
|
||||
('file', u'文件消息'), ('news', u'图文消息'), ('mpnews', u'微信后台图文消息')], u'消息类型',
|
||||
required=True, default='text')
|
||||
agentid = fields.Many2one('we.app', u'发送消息的企业应用', required=True)
|
||||
content = fields.Char(u'消息内容')
|
||||
media_id = fields.Char(u'媒体文件')
|
||||
title = fields.Char(u'标题')
|
||||
description = fields.Text(u'描述')
|
||||
articles = fields.Char(u'图文消息')
|
||||
url = fields.Char(u'点击后跳转的链接')
|
||||
picurl = fields.Char(u'图文消息的图片链接')
|
||||
thumb_media_id = fields.Char(u'图文消息缩略图')
|
||||
author = fields.Char(u'图文消息的作者')
|
||||
content_source_url = fields.Char(u'图文消息点击“阅读原文”之后的页面链接')
|
||||
news_content = fields.Char(u'图文消息的内容,支持html标签')
|
||||
digest = fields.Char(u'图文消息的描述')
|
||||
show_cover_pic = fields.Selection([('0', u'否'), ('1', u'是')], u'是否显示封面', default='0')
|
||||
safe = fields.Selection([('0', u'否'), ('1', u'是')], u'是否是保密消息', required=True, default='0')
|
||||
|
||||
def send_message(self):
|
||||
"""
|
||||
发送消息给关注企业号的用户
|
||||
:return:
|
||||
"""
|
||||
users = ""
|
||||
i = 0
|
||||
if self.touser and len(self.touser) > 0:
|
||||
for data in self.touser:
|
||||
i = i + 1
|
||||
if i == len(self.touser):
|
||||
if data['we_employee_id']:
|
||||
users = users + data['we_employee_id']
|
||||
else:
|
||||
if data['we_employee_id']:
|
||||
users = users + data['we_employee_id'] + "|"
|
||||
if users == "":
|
||||
users = '@all'
|
||||
partys = ""
|
||||
|
||||
if self.msgtype == "news":
|
||||
self.send_news_message(users, self.agentid['agentid'], self.title, self.description, self.url, self.picurl)
|
||||
elif self.msgtype == "text":
|
||||
self.send_text_message(users, self.agentid['agentid'], self.content, partys)
|
||||
|
||||
def send_text_message(self, userid, agentid, content, partyid=None):
|
||||
"""
|
||||
发送文本消息给关注企业号的用户
|
||||
:param userid:
|
||||
:param agentid:
|
||||
:param content:
|
||||
:param partyid:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
wechat = self.env['we.config'].sudo().get_wechat(agent_id=agentid)
|
||||
if wechat:
|
||||
data = {
|
||||
'safe': "0",
|
||||
'msgtype': 'text',
|
||||
'agentid': agentid,
|
||||
'touser': userid,
|
||||
'toparty': partyid,
|
||||
'content': content
|
||||
}
|
||||
wechat.message.send_text(agent_id=data['agentid'], user_ids=data['touser'], content=data['content'],
|
||||
party_ids=data['toparty'], safe=data['safe'])
|
||||
else:
|
||||
raise exceptions.Warning(u"初始化企业号失败")
|
||||
except Exception as e:
|
||||
logging.error('send_text_message:%s' % e)
|
||||
|
||||
# 发送图文消息给关注企业号的用户
|
||||
def send_news_message(self, userid, agentid, title=None, description=None, url=None, picurl=None):
|
||||
"""
|
||||
发送图文消息给关注企业号的用户
|
||||
:param userid:
|
||||
:param agentid:
|
||||
:param title:
|
||||
:param description:
|
||||
:param url:
|
||||
:param picurl:
|
||||
:return:
|
||||
"""
|
||||
wechat = self.env['we.config'].sudo().get_wechat(agent_id=agentid)
|
||||
if wechat:
|
||||
articles = [
|
||||
{
|
||||
'url': url,
|
||||
'image': picurl,
|
||||
'description': description,
|
||||
'title': title
|
||||
}
|
||||
]
|
||||
wechat.message.send_articles(agent_id=agentid, user_ids=userid, articles=articles)
|
||||
else:
|
||||
raise exceptions.Warning(u"初始化企业号失败")
|
||||
@@ -1,89 +0,0 @@
|
||||
from odoo import api, models, exceptions
|
||||
from odoo.http import request
|
||||
import logging
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseTools(models.Model):
|
||||
"""
|
||||
微信企业号工具类
|
||||
"""
|
||||
_name = 'we.tools'
|
||||
_description = '微信企业号工具类'
|
||||
|
||||
def get_media(self, media_id):
|
||||
"""
|
||||
通过media_id 获取媒体文件
|
||||
:param media_id: media id
|
||||
:return:
|
||||
"""
|
||||
wechat = self.env['we.config'].sudo().get_wechat()
|
||||
try:
|
||||
media = wechat.media.download(media_id=media_id)
|
||||
return {
|
||||
'errcode': 0,
|
||||
'errmsg': 'ok',
|
||||
'media': media.content
|
||||
}
|
||||
except Exception as ex:
|
||||
_logger.info(u'get media fail, message: {str(ex)}.')
|
||||
return {
|
||||
'errcode': 30001,
|
||||
'errmsg': str(ex)
|
||||
}
|
||||
|
||||
def get_jsapi_ticket(self):
|
||||
"""
|
||||
获取jsapi_ticket
|
||||
:return:
|
||||
"""
|
||||
if request.session.get('ticket') and request.session.get('ticket_time') \
|
||||
and int(time.time()) - request.session['ticket_time'] <= 7000:
|
||||
return {
|
||||
'errcode': 0,
|
||||
'errmsg': 'ok',
|
||||
'ticket': request.session['ticket']
|
||||
}
|
||||
wechat = self.env['we.config'].sudo().get_wechat()
|
||||
get_token = wechat.fetch_access_token()
|
||||
if get_token['errcode'] == 0 and get_token['errmsg'] == 'ok':
|
||||
access_token = get_token['access_token']
|
||||
url = u'https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token={access_token}'
|
||||
response = requests.get(url).json()
|
||||
if response['errcode'] == 0 and response['errmsg'] == 'ok':
|
||||
request.session['ticket'] = response['ticket']
|
||||
request.session['ticket_time'] = int(time.time())
|
||||
return response
|
||||
return {
|
||||
"errcode": 10002,
|
||||
"errmsg": "get ticket fail."
|
||||
}
|
||||
|
||||
def check_message_signature(self, message_list, msg_signature):
|
||||
"""
|
||||
校验消息的正确性
|
||||
:param message_list: 消息列表 (list: token, timestamp, nonce, echo_string)
|
||||
:param msg_signature: 签名
|
||||
:return: true or false
|
||||
"""
|
||||
_logger.info(u'check message signature.')
|
||||
message_list.sort()
|
||||
message_str = "".join(message_list)
|
||||
sha1_message = hashlib.sha1(str(message_str).encode('utf-8')).hexdigest()
|
||||
if sha1_message == msg_signature:
|
||||
return True
|
||||
return False
|
||||
|
||||
def decode_echo_str(self, echo_str):
|
||||
"""
|
||||
解密echo string 得到明文内容
|
||||
:param echo_str: 加密字符串
|
||||
:return: message
|
||||
"""
|
||||
_logger.info(u'decode echo string.')
|
||||
base64_str = base64.b64decode(echo_str)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user