├── data ├── __init__.py ├── crm_team_datas.xml ├── res_partner_category_datas.xml ├── payment_sequence.xml ├── wxapp_config_datas.xml ├── product_product_datas.xml └── oe_province_datas.py ├── ext_libs └── weixin │ ├── lib │ ├── __init__.py │ ├── ierror.py │ ├── wxcrypt.py │ ├── Sample.py │ └── WXBizMsgCrypt.py │ ├── config.py │ ├── json_import.py │ ├── __init__.py │ ├── msg_template.py │ ├── response.py │ ├── client.py │ ├── bind.py │ ├── reply.py │ └── helper.py ├── static ├── D4b0LrA2ln.txt ├── logo.png ├── poster_bg.png ├── weappshop │ ├── cart.png │ ├── gou.png │ ├── kefu.png │ ├── add-addr.png │ ├── addr-edit.png │ ├── addr-line.png │ ├── gou-red.png │ ├── ico-addr.png │ ├── icon-cart.png │ ├── addr-active.png │ ├── arrow-right.png │ ├── popup-close.png │ └── ico-add-addr.png └── description │ ├── icon.png │ ├── main.png │ ├── odoo_wxapp.jpg │ └── index.html ├── requirements.txt ├── .gitignore ├── __init__.py ├── models ├── oe_province.py ├── oe_city.py ├── wxapp_notice.py ├── __init__.py ├── oe_shipper.py ├── wxapp_confirm_wizard.py ├── wxapp_payment.py ├── wxapp_access_token.py ├── res_partner.py ├── wxapp_banner.py ├── wxapp_product_category.py ├── wxapp_user.py ├── oe_district.py ├── product.py ├── wxapp_config.py └── sale_order.py ├── controllers ├── message.py ├── tools.py ├── score.py ├── region.py ├── __init__.py ├── notice.py ├── product_category.py ├── config.py ├── banner.py ├── base.py ├── address.py ├── product.py └── user.py ├── security ├── res_groups.xml └── ir.model.access.csv ├── views ├── parent_menus.xml ├── wxapp_confirm_views.xml ├── oe_city_views.xml ├── oe_province_views.xml ├── oe_shipper_views.xml ├── oe_district_views.xml ├── wxapp_notice_views.xml ├── wxapp_config_views.xml ├── wxapp_product_category_views.xml ├── wxapp_banner_views.xml ├── wxapp_payment_views.xml ├── wxapp_user_views.xml ├── product_template_views.xml └── sale_order_views.xml ├── __manifest__.py ├── od13.py ├── defs.py ├── README.md └── const.py /data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ext_libs/weixin/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/D4b0LrA2ln.txt: -------------------------------------------------------------------------------- 1 | 4bebcf7c3014385084c74c9760ee355d -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome 2 | xmltodict==0.11.0 3 | itsdangerous==0.24 4 | -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/logo.png -------------------------------------------------------------------------------- /static/poster_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/poster_bg.png -------------------------------------------------------------------------------- /static/weappshop/cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/cart.png -------------------------------------------------------------------------------- /static/weappshop/gou.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/gou.png -------------------------------------------------------------------------------- /static/weappshop/kefu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/kefu.png -------------------------------------------------------------------------------- /static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/description/icon.png -------------------------------------------------------------------------------- /static/description/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/description/main.png -------------------------------------------------------------------------------- /static/weappshop/add-addr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/add-addr.png -------------------------------------------------------------------------------- /static/weappshop/addr-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/addr-edit.png -------------------------------------------------------------------------------- /static/weappshop/addr-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/addr-line.png -------------------------------------------------------------------------------- /static/weappshop/gou-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/gou-red.png -------------------------------------------------------------------------------- /static/weappshop/ico-addr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/ico-addr.png -------------------------------------------------------------------------------- /static/weappshop/icon-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/icon-cart.png -------------------------------------------------------------------------------- /static/weappshop/addr-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/addr-active.png -------------------------------------------------------------------------------- /static/weappshop/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/arrow-right.png -------------------------------------------------------------------------------- /static/weappshop/popup-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/popup-close.png -------------------------------------------------------------------------------- /static/description/odoo_wxapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/description/odoo_wxapp.jpg -------------------------------------------------------------------------------- /static/weappshop/ico-add-addr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoneXiong/oejia_weshop/HEAD/static/weappshop/ico-add-addr.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # C extensions 4 | *.so 5 | 6 | 7 | # Mr Developer 8 | .mr.developer.cfg 9 | .project 10 | .pydevproject 11 | -------------------------------------------------------------------------------- /ext_libs/weixin/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | AUTO_REPLY_CONTENT = ''' 5 | 欢迎关注在行! 6 | 「在行」是一个知识服务的平台。我们为想要解决问题的人找到行家,一对一面对面答疑解惑、出谋划策;我们为愿意分享知识和经验的人找到学员,让头脑中的资源发挥更大的价值。 7 | 期待你的加入! 8 | www.zaih.com 9 | ''' 10 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | cur_dir = os.path.abspath(os.path.join( os.path.dirname(__file__) ) ) 5 | ext_path = os.path.join(cur_dir, 'ext_libs') 6 | sys.path.append(ext_path) 7 | 8 | from . import od13 9 | from . import controllers 10 | from . import models 11 | -------------------------------------------------------------------------------- /data/crm_team_datas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 电商网销 7 | True 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /models/oe_province.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | 6 | class Province(models.Model): 7 | 8 | _name = 'oe.province' 9 | _description = u'省份' 10 | 11 | name = fields.Char('名称', requried=True) 12 | child_ids = fields.One2many('oe.city', 'pid', string='市') 13 | -------------------------------------------------------------------------------- /ext_libs/weixin/json_import.py: -------------------------------------------------------------------------------- 1 | try: 2 | import simplejson 3 | except ImportError: 4 | try: 5 | import json as simplejson 6 | except ImportError: 7 | try: 8 | from django.utils import simplejson 9 | except ImportError: 10 | raise ImportError('A json library is required to use this python library') 11 | -------------------------------------------------------------------------------- /ext_libs/weixin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __title__ = 'requests' 4 | __version__ = '0.0.2' 5 | __author__ = 'Zongxiao Cheng' 6 | __license__ = 'BSD' 7 | 8 | 9 | from .bind import WeixinClientError, WeixinAPIError 10 | from .client import WeixinAPI, WeixinMpAPI, WXAPPAPI 11 | from .response import WXResponse 12 | from .reply import WXReply 13 | -------------------------------------------------------------------------------- /models/oe_city.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | 6 | class City(models.Model): 7 | 8 | _name = 'oe.city' 9 | _description = u'城市' 10 | 11 | pid = fields.Many2one('oe.province', string='省份') 12 | name = fields.Char('名称', requried=True) 13 | child_ids = fields.One2many('oe.district', 'pid', string='区') 14 | -------------------------------------------------------------------------------- /data/res_partner_category_datas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 商城客户 7 | True 8 | 1 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /models/wxapp_notice.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | 6 | class WxappNotice(models.Model): 7 | 8 | _name = 'wxapp.notice' 9 | _description = u'公告' 10 | _rec_name = 'title' 11 | 12 | title = fields.Char(string='标题', required=True) 13 | content = fields.Text('内容') 14 | active = fields.Boolean('是否有效', default=True) 15 | -------------------------------------------------------------------------------- /data/payment_sequence.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wxapp payment num 7 | wxapp.payment_num 8 | 4 9 | TS%(y)s%(month)s%(day)s%(h24)s%(min)s%(sec)s 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /data/wxapp_config_datas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xxxxxxxxxxxxxxxx 7 | xxxxxxxxxxxxxxxxxxxxxx 8 | OE商城 9 | p 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import oe_province 4 | from . import oe_city 5 | from . import oe_district 6 | from . import oe_shipper 7 | 8 | from . import res_partner 9 | from . import product 10 | from . import sale_order 11 | 12 | from . import wxapp_config 13 | from . import wxapp_user 14 | from . import wxapp_access_token 15 | from . import wxapp_banner 16 | from . import wxapp_product_category 17 | from . import wxapp_payment 18 | from . import wxapp_confirm_wizard 19 | from . import wxapp_notice 20 | 21 | -------------------------------------------------------------------------------- /models/oe_shipper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | from .. import defs 6 | 7 | 8 | class Shipper(models.Model): 9 | 10 | _name = 'oe.shipper' 11 | _description = u'物流商' 12 | 13 | name = fields.Char('名称') 14 | code = fields.Char('编码') 15 | 16 | 17 | @api.model_cr 18 | def init(self): 19 | from ..data.oe_shipper_datas import init_sql 20 | self.env.cr.execute(init_sql) 21 | self.env.cr.execute("select setval('oe_shipper_id_seq', max(id)) from oe_shipper;") 22 | 23 | -------------------------------------------------------------------------------- /controllers/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | 8 | from .. import defs 9 | from .base import BaseController 10 | 11 | import logging 12 | 13 | _logger = logging.getLogger(__name__) 14 | 15 | 16 | class WxappMessage(http.Controller, BaseController): 17 | 18 | @http.route('/wxa//template-msg/wxa/formId', auth='public', method=['POST'], csrf=False) 19 | def save_formid(self, sub_domain, token, formId=None, type=None, **kwargs): 20 | return self.res_ok() 21 | 22 | @http.route('/wxa//template-msg/put', auth='public', methods=['POST'], csrf=False, type='http') 23 | def send_template_msg(self, sub_domain, **kwargs): 24 | return self.res_ok() 25 | 26 | -------------------------------------------------------------------------------- /data/product_product_datas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 运费 7 | Delivery_Weshop 8 | service 9 | 10 | 11 | 10.0 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ext_libs/weixin/lib/ierror.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ######################################################################### 4 | # Author: jonyqin 5 | # Created Time: Thu 11 Sep 2014 01:53:58 PM CST 6 | # File Name: ierror.py 7 | # Description:定义错误码含义 8 | ######################################################################### 9 | WXBizMsgCrypt_OK = 0 10 | WXBizMsgCrypt_ValidateSignature_Error = -40001 11 | WXBizMsgCrypt_ParseXml_Error = -40002 12 | WXBizMsgCrypt_ComputeSignature_Error = -40003 13 | WXBizMsgCrypt_IllegalAesKey = -40004 14 | WXBizMsgCrypt_ValidateAppid_Error = -40005 15 | WXBizMsgCrypt_EncryptAES_Error = -40006 16 | WXBizMsgCrypt_DecryptAES_Error = -40007 17 | WXBizMsgCrypt_IllegalBuffer = -40008 18 | WXBizMsgCrypt_EncodeBase64_Error = -40009 19 | WXBizMsgCrypt_DecodeBase64_Error = -40010 20 | WXBizMsgCrypt_GenReturnXml_Error = -40011 21 | -------------------------------------------------------------------------------- /security/res_groups.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 电商 6 | False 7 | 0 8 | 9 | 10 | 11 | 12 | 电商配置 13 | 14 | 17 | 18 | 19 | 20 | 电商销售 21 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /views/parent_menus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /controllers/tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from weixin.lib.wxcrypt import WXBizDataCrypt 4 | from weixin import WXAPPAPI 5 | from weixin.oauth2 import OAuth2AuthExchangeError 6 | 7 | 8 | def get_wx_session_info(app_id, secret, code): 9 | api = WXAPPAPI(appid=app_id, app_secret=secret) 10 | try: 11 | session_info = api.exchange_code_for_session_key(code=code) 12 | except OAuth2AuthExchangeError as e: 13 | raise e 14 | return session_info 15 | 16 | 17 | def get_wx_user_info(app_id, secret, code, encrypted_data, iv): 18 | session_info = get_wx_session_info(app_id, secret, code) 19 | session_key = session_info.get('session_key') 20 | crypt = WXBizDataCrypt(app_id, session_key) 21 | user_info = crypt.decrypt(encrypted_data, iv) 22 | return session_key, user_info 23 | 24 | def get_decrypt_info(app_id, session_key, encrypted_data, iv): 25 | crypt = WXBizDataCrypt(app_id, session_key) 26 | _info = crypt.decrypt(encrypted_data, iv) 27 | return _info 28 | 29 | -------------------------------------------------------------------------------- /models/wxapp_confirm_wizard.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from odoo import models, fields, api 4 | 5 | 6 | class WxConfirm(models.TransientModel): 7 | 8 | _name = 'wxapp.confirm' 9 | _description = u'确认' 10 | 11 | info = fields.Text("信息") 12 | model = fields.Char('模型') 13 | method = fields.Char('方法') 14 | 15 | api.multi 16 | def execute(self): 17 | self.ensure_one() 18 | active_ids = self._context.get('record_ids') 19 | if not active_ids: 20 | active_ids = self._context.get('active_ids') 21 | rs = self.env[self.model].browse(active_ids) 22 | ret = getattr(rs, self.method)() 23 | return ret 24 | 25 | api.multi 26 | def execute_with_info(self): 27 | self.ensure_one() 28 | active_ids = self._context.get('record_ids') 29 | if not active_ids: 30 | active_ids = self._context.get('active_ids') 31 | rs = self.env[self.model].browse(active_ids) 32 | ret = getattr(rs, self.method)(self.info) 33 | return ret 34 | -------------------------------------------------------------------------------- /ext_libs/weixin/lib/wxcrypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 对小程序获取的用户信息解密代码. 5 | """ 6 | import base64 7 | from Crypto.Cipher import AES 8 | 9 | from ..json_import import simplejson as json 10 | 11 | 12 | class WXBizDataCrypt: 13 | 14 | def __init__(self, appid, session_key): 15 | self.appid = appid 16 | self.session_key = session_key 17 | 18 | def decrypt(self, encrypted_data, iv): 19 | ''' 20 | aes decode 21 | 将加密后的信息解密 22 | @param encrypted_data: 包括敏感数据在内的完整用户信息的加密数据 23 | @param iv: 加密算法的初始向量 24 | @return: 解密后数据 25 | ''' 26 | session_key = base64.b64decode(self.session_key) 27 | encrypted_data = base64.b64decode(encrypted_data) 28 | iv = base64.b64decode(iv) 29 | 30 | cipher = AES.new(session_key, AES.MODE_CBC, iv) 31 | 32 | _json = self._unpad(cipher.decrypt(encrypted_data)).decode('utf-8') 33 | decrypted = json.loads(_json) 34 | 35 | if decrypted['watermark']['appid'] != self.appid: 36 | raise Exception('Invalid Buffer') 37 | 38 | return decrypted 39 | 40 | def _unpad(self, s): 41 | return s[:-ord(s[len(s)-1:])] 42 | -------------------------------------------------------------------------------- /static/description/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 详细介绍 https://github.com/JoneXiong/oejia_weshop/blob/master/README.md 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 联系我们 15 | 16 | 项目主页: https://github.com/JoneXiong/oejia_weshop 17 | 18 | 19 | 官网网站: www.oejia.net 20 | 21 | 22 | 文档说明: oejia_weshop_document.html 23 | 24 | 25 | 联 系: 微信 johan-x | Q 669229467 | Email odoo@calluu.com | Q群 260160505 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /models/wxapp_payment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | from .. import defs 6 | 7 | 8 | class Payment(models.Model): 9 | 10 | _name = 'wxapp.payment' 11 | _description = u'支付记录' 12 | _order = 'id desc' 13 | 14 | wechat_user_id = fields.Many2one('wxapp.user', string='客户') 15 | order_id = fields.Many2one('sale.order', string='订单') 16 | payment_number = fields.Char('支付单号', index=True) 17 | price = fields.Float('支付金额(元)') 18 | status = fields.Selection(defs.PaymentStatus.attrs.items(), string='状态', default=defs.PaymentStatus.unpaid) 19 | 20 | # notify返回参数 21 | openid = fields.Char('openid') 22 | result_code = fields.Char('业务结果') 23 | err_code = fields.Char('错误代码') 24 | err_code_des = fields.Char('错误代码描述') 25 | transaction_id = fields.Char('订单号') 26 | bank_type = fields.Char('付款银行') 27 | fee_type = fields.Char('货币种类') 28 | total_fee = fields.Integer('订单金额(分)') 29 | settlement_total_fee = fields.Integer('应结订单金额(分)') 30 | cash_fee = fields.Integer('现金支付金额') 31 | cash_fee_type = fields.Char('货币类型') 32 | coupon_fee = fields.Integer('代金券金额(分)') 33 | coupon_count = fields.Integer('代金券使用数量') 34 | 35 | _sql_constraints = [( 36 | 'wxapp_payment_payment_number_unique', 37 | 'UNIQUE (payment_number)', 38 | 'wechat payment payment_number is existed!' 39 | )] 40 | 41 | -------------------------------------------------------------------------------- /controllers/score.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | 8 | from .. import defs 9 | from .base import BaseController 10 | 11 | import logging 12 | 13 | _logger = logging.getLogger(__name__) 14 | 15 | 16 | class WxappScore(http.Controller, BaseController): 17 | 18 | @http.route('/wxa//score/send/rule', auth='public', methods=['GET', 'POST'], csrf=False) 19 | def list(self, sub_domain, code=5, **kwargs): 20 | try: 21 | ret, entry = self._check_domain(sub_domain) 22 | if ret:return ret 23 | 24 | data = [] 25 | 26 | return self.res_err(700) 27 | 28 | except Exception as e: 29 | _logger.exception(e) 30 | return self.res_err(-1, str(e)) 31 | 32 | @http.route('/wxa//shop/goods/kanjia/list', auth='public', methods=['GET', 'POST'], csrf=False) 33 | def kanjia_list(self, sub_domain, **kwargs): 34 | try: 35 | ret, entry = self._check_domain(sub_domain) 36 | if ret:return ret 37 | 38 | data = { 39 | 'result': [], 40 | 'goodsMap': {}, 41 | } 42 | 43 | return self.res_ok(data) 44 | 45 | except Exception as e: 46 | _logger.exception(e) 47 | return self.res_err(-1, str(e)) 48 | 49 | -------------------------------------------------------------------------------- /models/wxapp_access_token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | 5 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 6 | from odoo import models, fields, api, exceptions 7 | 8 | 9 | class AccessToken(models.TransientModel): 10 | 11 | _name = 'wxapp.access_token' 12 | _description = u'assess token' 13 | 14 | # allow session to survive for 30min in case user is slow 15 | _transient_max_hours = 24 16 | 17 | token = fields.Char('token', index=True) 18 | session_key = fields.Char('session_key', required=True) 19 | open_id = fields.Char('open_id', required=True) 20 | 21 | @api.model 22 | def create(self, vals): 23 | record = super(AccessToken, self).create(vals) 24 | record.write({'token': record.generate_token(vals['sub_domain'])}) 25 | return record 26 | 27 | def generate_token(self, sub_domain): 28 | entry = self.env['wxapp.config'].get_entry(sub_domain) 29 | secret_key = entry.get_config('secret') 30 | app_id = entry.get_config('app_id') 31 | if not secret_key or not app_id: 32 | raise exceptions.ValidationError('未设置 secret_key 或 appId') 33 | 34 | s = Serializer(secret_key=secret_key, salt=app_id, expires_in=AccessToken._transient_max_hours * 3600) 35 | timestamp = time.time() 36 | return s.dumps({'session_key': self.session_key, 'open_id': self.open_id, 'iat': timestamp}) 37 | -------------------------------------------------------------------------------- /__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | 'name': "OE商城", 4 | 'version': '1.0.0', 5 | 'category': '', 6 | 'summary': 'Odoo OE商城,电商、小程序商城', 7 | 'author': 'Oejia', 8 | 'website': 'http://www.oejia.net/', 9 | 'depends': ['base', 'mail', 'sale'], 10 | 'external_dependencies': { 11 | 'python': ['Crypto', 'xmltodict', 'itsdangerous'], 12 | }, 13 | 'data': [ 14 | 'security/res_groups.xml', 15 | 'security/ir.model.access.csv', 16 | 17 | 'views/parent_menus.xml', 18 | 19 | 'data/crm_team_datas.xml', 20 | 'views/oe_shipper_views.xml', 21 | 'views/oe_province_views.xml', 22 | 'views/oe_city_views.xml', 23 | 'views/oe_district_views.xml', 24 | 25 | 'views/wxapp_config_views.xml', 26 | 'views/wxapp_banner_views.xml', 27 | 'views/wxapp_user_views.xml', 28 | 'views/wxapp_product_category_views.xml', 29 | 'views/wxapp_payment_views.xml', 30 | 'views/wxapp_confirm_views.xml', 31 | 'views/wxapp_notice_views.xml', 32 | 33 | 'views/product_template_views.xml', 34 | 'views/sale_order_views.xml', 35 | 36 | 'data/wxapp_config_datas.xml', 37 | 'data/product_product_datas.xml', 38 | 'data/res_partner_category_datas.xml', 39 | 40 | ], 41 | 'demo': [ 42 | ], 43 | 'images': [], 44 | 'description': """oejia_weshop 是 Odoo 电商基础模块,对接微信小程序实现的微商城应用 45 | """, 46 | 'license': 'GPL-3', 47 | } 48 | -------------------------------------------------------------------------------- /views/wxapp_confirm_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wxapp.confirm.view_form 7 | wxapp.confirm 8 | form 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | wxapp.confirm.view_form_send 21 | wxapp.confirm 22 | form 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /od13.py: -------------------------------------------------------------------------------- 1 | import logging 2 | _logger = logging.getLogger(__name__) 3 | 4 | def multi(method): 5 | method._api = 'multi' 6 | return method 7 | 8 | def model_cr(method): 9 | method._api = 'model_cr' 10 | return method 11 | 12 | from odoo import api 13 | api.multi = multi 14 | api.model_cr = model_cr 15 | try: 16 | from odoo import api 17 | api.multi = multi 18 | api.model_cr = model_cr 19 | except: 20 | import traceback;traceback.print_exc() 21 | 22 | 23 | from odoo import models 24 | origin_write = models.BaseModel.write 25 | def write(self, vals): 26 | _vals = {} 27 | for k,v in vals.items(): 28 | if k in self._fields: 29 | _vals[k] = v 30 | else: 31 | _logger.warning('>>> odoo 13 hook: model %s has no field %s', self._name, k) 32 | #vals = { k:v for k,v in vals.items() if k in self._fields} 33 | return origin_write(self, _vals) 34 | models.BaseModel.write = write 35 | 36 | origin_create = models.BaseModel.create 37 | @api.model_create_multi 38 | def create(self, vals_list): 39 | _vals_list = [] 40 | for vals in vals_list: 41 | _vals = {} 42 | for k,v in vals.items(): 43 | if k in self._fields: 44 | _vals[k] = v 45 | else: 46 | _logger.warning('>>> odoo 13 hook: model %s has no field %s', self._name, k) 47 | #vals = { k:v for k,v in vals.items() if k in self._fields} 48 | _vals_list.append(_vals) 49 | return origin_create(self, _vals_list) 50 | models.BaseModel.create = create 51 | -------------------------------------------------------------------------------- /models/res_partner.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from odoo import models, fields, api 4 | 5 | 6 | class res_partner(models.Model): 7 | 8 | _inherit = 'res.partner' 9 | 10 | province_id = fields.Many2one('oe.province', string='省') 11 | city_id = fields.Many2one('oe.city', string='市') 12 | district_id = fields.Many2one('oe.district', string='区') 13 | # street 详细地址 14 | is_default = fields.Boolean('是否为默认地址') 15 | city_domain_ids = fields.One2many('oe.city', compute='_compute_city_domain_ids') 16 | district_domain_ids = fields.One2many('oe.district', compute='_compute_district_domain_ids') 17 | 18 | 19 | @api.onchange('province_id') 20 | def _onchange_province_id(self): 21 | self.city_domain_ids = self.province_id.child_ids if self.province_id else False 22 | self.city_id = False 23 | self.district_id = False 24 | return { 25 | 'domain': { 26 | 'city_id': [('id', 'in', self.city_domain_ids.ids if self.city_domain_ids else [0])] 27 | } 28 | } 29 | 30 | @api.onchange('city_id') 31 | def _onchange_city_id(self): 32 | self.district_domain_ids = self.city_id.child_ids if self.city_id else False 33 | self.district_id = False 34 | return { 35 | 'domain': { 36 | 'district_id': [('id', 'in', self.district_domain_ids.ids if self.district_domain_ids else [0])] 37 | } 38 | } 39 | 40 | @api.depends('province_id') 41 | def _compute_city_domain_ids(self): 42 | for obj in self: 43 | obj.city_domain_ids = obj.province_id.child_ids if obj.province_id else False 44 | 45 | @api.depends('city_id') 46 | def _compute_district_domain_ids(self): 47 | for obj in self: 48 | obj.district_domain_ids = obj.city_id.child_ids if obj.city_id else False 49 | -------------------------------------------------------------------------------- /models/wxapp_banner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | from .. import defs 6 | 7 | 8 | class Banner(models.Model): 9 | 10 | _name = 'wxapp.banner' 11 | _description = u'横幅图' 12 | _rec_name = 'title' 13 | _order = 'sort' 14 | 15 | title = fields.Char(string='名称', required=True) 16 | display_pic = fields.Html('图片', compute='_compute_display_pic') 17 | image = fields.Binary(string='图片') 18 | link_type = fields.Selection([('no', '无'), ('business', '跳转商品'), ('page', '跳转内部页面'), ('url', '跳转URL')], string='链接跳转类型', default='no') 19 | business_id = fields.Many2one('product.template', string='链接商品') 20 | link_page = fields.Char(string='页面路径') 21 | link_url = fields.Char(string='URL地址') 22 | sort = fields.Integer(string='排序') 23 | status = fields.Boolean('显示', default=True) 24 | remark = fields.Text(string='备注') 25 | 26 | type_mark = fields.Integer(string='类型标记', default=0) 27 | ptype = fields.Selection([('index', '首页顶部'), ('app', '启动页')], string='位置', default='index') 28 | ctype = fields.Selection([('1', '移动端')], string='终端类型', default='1') 29 | 30 | @api.depends('image') 31 | def _compute_display_pic(self): 32 | for each_record in self: 33 | if each_record.image: 34 | each_record.display_pic = """""".format(pic=each_record.get_main_image()) 35 | else: 36 | each_record.display_pic = False 37 | 38 | def get_main_image(self): 39 | base_url=self.env['ir.config_parameter'].sudo().get_param('web.base.url') 40 | return '%s/web/image/wxapp.banner/%s/image/'%(base_url, self.id) 41 | 42 | def fetch_url(self, partner): 43 | return 44 | 45 | def get_business_id(self): 46 | if self.link_type=='business': 47 | return self.business_id.id 48 | else: 49 | return self.id 50 | -------------------------------------------------------------------------------- /defs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | from .const import Const 5 | 6 | 7 | class GoodsRecommendStatus(Const): 8 | normal = (False, u'普通') 9 | recommend = (True, u'推荐') 10 | 11 | 12 | class OrderStatus(Const): 13 | closed = ('closed', u'已关闭') 14 | unpaid = ('unpaid', u'待支付') 15 | pending = ('pending', u'待发货') 16 | unconfirmed = ('unconfirmed', u'待收货') 17 | unevaluated = ('unevaluated', u'待评价') 18 | completed = ('completed', u'已完成') 19 | 20 | class OrderRequestStatus(Const): 21 | closed = (-1, 'closed') 22 | unpaid = (0, 'unpaid') 23 | pending = (1, 'pending') 24 | unconfirmed = (2, 'unconfirmed') 25 | unevaluated = (3, 'unevaluated') 26 | completed = (4, 'completed') 27 | 28 | class OrderResponseStatus(Const): 29 | closed = ('closed', -1) 30 | unpaid = ('unpaid', 0) 31 | pending = ('pending', 1) 32 | unconfirmed = ('unconfirmed', 2) 33 | unevaluated = ('unevaluated', 3) 34 | completed = ('completed', 4) 35 | 36 | 37 | class BannerStatus(Const): 38 | visible = (True, u'显示') 39 | invisible = (False, u'不显示') 40 | 41 | class WechatUserRegisterType(Const): 42 | app = ('app', u'小程序') 43 | gzh = ('gzh', u'公众号') 44 | sys = ('sys', u'系统注册/登录') 45 | 46 | class WechatUserStatus(Const): 47 | default = ('default', u'默认') 48 | 49 | class PaymentStatus(Const): 50 | unpaid = ('unpaid', '未支付') 51 | success = ('success', '成功') 52 | fail = ('fail', '失败') 53 | 54 | 55 | 56 | def hump2underline(hunp_str): 57 | ''' 58 | 驼峰形式字符串转成下划线形式 59 | :param hunp_str: 驼峰形式字符串 60 | :return: 字母全小写的下划线形式字符串 61 | ''' 62 | p = re.compile(r'([a-z]|\d)([A-Z])') 63 | sub = re.sub(p, r'\1_\2', hunp_str).lower() 64 | return sub 65 | 66 | def underline2hump(underline_str): 67 | ''' 68 | 下划线形式字符串转成驼峰形式 69 | :param underline_str: 下划线形式字符串 70 | :return: 驼峰形式字符串 71 | ''' 72 | sub = re.sub(r'(_\w)',lambda x:x.group(1)[1].upper(),underline_str) 73 | return sub 74 | 75 | def get_precision(): 76 | return 16, 2 77 | -------------------------------------------------------------------------------- /models/wxapp_product_category.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | 6 | class Category(models.Model): 7 | 8 | _name = 'wxapp.product.category' 9 | _description = u'商品分类' 10 | _order = 'level,sort' 11 | _rec_name = 'complete_name' 12 | 13 | name = fields.Char(string='名称', required=True) 14 | complete_name = fields.Char(string='全名', compute='_compute_complete_name', store=True, recursive=True) 15 | category_type = fields.Char(string='类型') 16 | pid = fields.Many2one('wxapp.product.category', string='上级分类', ondelete='cascade') 17 | child_ids = fields.One2many('wxapp.product.category', 'pid', string='子分类') 18 | key = fields.Char(string='编号') 19 | icon = fields.Binary(string='图标/图片') 20 | level = fields.Integer(string='分类级别', compute='_compute_level', store=True) 21 | is_use = fields.Boolean(string='是否启用', default=True) 22 | index_display = fields.Boolean(string='首页导航展示', default=True) 23 | sort = fields.Integer(string='排序') 24 | product_template_ids = fields.One2many('product.template', 'wxpp_category_id', string='商品') 25 | 26 | @api.depends('pid') 27 | def _compute_level(self): 28 | for cate in self: 29 | level = 0 30 | pid = cate.pid 31 | while True: 32 | if not pid: 33 | break 34 | 35 | pid = pid.pid 36 | 37 | level += 1 38 | 39 | cate.level = level 40 | for child in cate.child_ids: 41 | child._compute_level() 42 | 43 | @api.depends('name','pid.complete_name') 44 | def _compute_complete_name(self): 45 | for cate in self: 46 | if cate.pid: 47 | cate.complete_name = '%s / %s'%(cate.pid.complete_name, cate.name) 48 | else: 49 | cate.complete_name = cate.name 50 | 51 | def get_icon_image(self): 52 | base_url=self.env['ir.config_parameter'].sudo().get_param('web.base.url') 53 | return '%s/web/image/wxapp.product.category/%s/icon/'%(base_url, self.id) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## oejia_weshop 2 | 3 | oejia_weshop(OE商城) 是一套包含强大电商ERP后台的小程序商城系统。 4 | 5 | oejia_weshop 是 Odoo 对接微信小程序实现的商城应用。 6 | 7 | 如果您想要搭建一套进销存(ERP)系统并实现微信商城及完整的电商管理后台,用OE商城系统(Odoo + oejia_weshop 等模块)是个不错的选择,强大的生态,灵活的架构,可适应未来各种新的在线商业模式 8 | 9 | 如果您已使用odoo系统,而想要在微信小程序上实现自己的独立的微商城卖odoo里的商品,装上 oejia_weshop 模块即可,无额外的数据迁移之类的工作。 10 | 11 | ## 特性 12 | * 和 odoo 销售模块无缝集成,产品和订单统一管理 13 | * 微信用户集成到 odoo 统一的客户(partner)管理 14 | * 支持 Odoo 10.0、11.0、12.0 15 | 16 | ## 使用 17 | 1. 下载源码 (odoo10、11为master分支,odoo12为12.0分支) 18 | 2. 将整个oejia_weshop目录(名称不能变)放到你的 addons 目录下 19 | 3. 安装依赖的python库:xmltodict、pycrypto、itsdangerous;安装模块,可以看到产生了顶部“小程序”主菜单 20 | 4. 进入【设置】-【对接设置】页填写你的微信小程序相关对接信息 21 | 5. 小程序客户端: 见项目 [wechat-app-mall](https://github.com/JoneXiong/wechat-app-mall), 下载后修改接口api调用路径为您的odoo url即可,可参考[这里](https://github.com/JoneXiong/wechat-app-mall/blob/f2/README.md)修改 22 | 23 | 参考资料: [常见问题处理](http://oejia.net/blog/2018/12/21/oejia_weshop_qa.html) 24 | 25 | ## 试用 26 | 27 | 小程序客户端 28 | 29 |  30 | 31 | Odoo后台 32 | 33 | [https://sale.calluu.cn/](https://sale.calluu.cn/) 34 | 35 | ## 效果图 36 | 37 | 详见 [http://oejia.net/blog/2018/09/13/oejia_weshop_about.html](http://oejia.net/blog/2018/09/13/oejia_weshop_about.html) 38 | 39 |  40 | 41 |  42 | 43 |  44 | 45 |  46 | 47 | 48 |  49 | 50 | ## 商业版及扩展 51 | 52 | 商业扩展模块 [oejia_weshop_ent](https://www.calluu.cn/shop/product/odoo-12) 53 | 54 | 分销模块 [weshop_commission](https://www.calluu.cn/shop/product/odoo-23) 55 | 56 | H5商城模块 [weshop_h5](https://www.calluu.cn/shop/product/odoo-h5-24) 57 | 58 | OE商城系统(全功能进销存系统套件) [https://sale.calluu.cn/](https://sale.calluu.cn/) 59 | 60 | ## 交流 61 | 技术分享 62 | [http://www.oejia.net/](http://www.oejia.net/) 63 | 64 | Odoo-OpenERP扩展开发3群:713722419 65 | 66 | Odoo-OpenERP扩展开发2群:796367461 (已满) 67 | 68 | Odoo-OpenERP扩展开发1群:260160505 (已满) 69 | -------------------------------------------------------------------------------- /views/oe_city_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oe.city.view_tree 7 | oe.city 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | oe.city.view_form 19 | oe.city 20 | form 21 | 999 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 市 36 | oe.city 37 | form 38 | tree,form 39 | current 40 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /views/oe_province_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oe.province.view_tree 7 | oe.province 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | oe.province.view_form 18 | oe.province 19 | form 20 | 999 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 省 34 | oe.province 35 | form 36 | tree,form 37 | current 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /views/oe_shipper_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oe.shipper.view_tree 7 | oe.shipper 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | oe.shipper.view_form 19 | oe.shipper 20 | form 21 | 999 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 承运商 35 | oe.shipper 36 | form 37 | tree,form 38 | current 39 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /views/oe_district_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oe.district.view_tree 7 | oe.district 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | oe.district.view_form 19 | oe.district 20 | form 21 | 999 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 区 35 | oe.district 36 | form 37 | tree,form 38 | current 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /views/wxapp_notice_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wxapp.notice.view_tree 7 | wxapp.notice 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | wxapp.notice.view_form 19 | wxapp.notice 20 | form 21 | 999 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 公告 36 | wxapp.notice 37 | form 38 | tree,form 39 | current 40 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /controllers/region.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | 8 | from .base import BaseController 9 | 10 | import logging 11 | 12 | _logger = logging.getLogger(__name__) 13 | 14 | 15 | class Region(http.Controller, BaseController): 16 | 17 | @http.route('/wxa/common/region/v2/province', auth='public', methods=['GET']) 18 | def province(self, **kwargs): 19 | provinces = request.env['oe.province'].sudo().search([]).sorted(key=lambda o: o.name[0]) 20 | data = [{'id': e.id, 'name': e.name, 'level': 1} for e in provinces] 21 | return self.res_ok(data) 22 | 23 | @http.route('/wxa/common/region/v2/child', auth='public', methods=['GET']) 24 | def child(self, pid, **kwargs): 25 | model = None 26 | level = 0 27 | if pid[-4:]=='0000' or int(pid)>820000: 28 | model = 'oe.city' 29 | level = 2 30 | else: 31 | level = 3 32 | model = 'oe.district' 33 | 34 | if model: 35 | objs = request.env[model].sudo().search([('pid', '=', int(pid))]).sorted(key=lambda o: o.name[0]) 36 | data = [{'id': e.id, 'name': e.name, 'level': level, 'pid': e.pid} for e in objs] 37 | return self.res_ok(data) 38 | else: 39 | return self.res_ok([{'id': 0, 'name':' ', 'pid': pid}]) 40 | 41 | @http.route('/wxa/common/region/v2/search', auth='public', methods=['POST'], csrf=False) 42 | def search(self, nameLike=False, **kwargs): 43 | if nameLike: 44 | objs = request.env['oe.district'].sudo().search([('name', 'ilike', nameLike)]).sorted(key=lambda o: o.name[0]) 45 | if not objs: 46 | citys = request.env['oe.city'].sudo().search([('name', 'ilike', nameLike)]).sorted(key=lambda o: o.name[0]) 47 | objs = [] 48 | for city in citys: 49 | objs = objs + [e for e in city.child_ids] 50 | data = [{'value': {'dObject': {'id': e.id,'name': e.name}, 'cObject': {'id': e.pid.id, 'name': e.pid.name}, 'pObject': {'id': e.pid.pid.id, 'name': e.pid.pid.name}}, 'text': '%s %s %s'%(e.pid.pid.name, e.pid.name, e.name)} for e in objs] 51 | return self.res_ok(data) 52 | else: 53 | return self.res_ok([]) 54 | -------------------------------------------------------------------------------- /controllers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from types import MethodType 5 | from odoo import release 6 | 7 | from . import base 8 | from . import config 9 | from . import banner 10 | from . import product_category 11 | from . import product 12 | from . import user 13 | from . import address 14 | from . import order 15 | from . import notice 16 | from . import score 17 | from . import region 18 | from . import message 19 | 20 | _logger = logging.getLogger(__name__) 21 | 22 | if release.version_info[0]>=16: 23 | from odoo.http import Request 24 | origin_default_lang = Request.default_lang 25 | def default_lang(self): 26 | httprequest = self.httprequest 27 | if 'Referer' in httprequest.headers and 'servicewechat.com' in httprequest.headers['Referer']: 28 | _logger.info('>>> default_lang %s', httprequest.accept_languages.best) 29 | if not httprequest.accept_languages.best: 30 | return 'zh_CN' 31 | return origin_default_lang(self) 32 | Request.default_lang = default_lang 33 | else: 34 | from odoo.http import root, JsonRequest, HttpRequest 35 | def get_request(self, httprequest): 36 | if 'Referer' in httprequest.headers and 'servicewechat.com' in httprequest.headers['Referer']: 37 | if not httprequest.accept_languages.best: 38 | httprequest.session.context["lang"] = 'zh_CN' 39 | if httprequest.mimetype=="application/json": 40 | return HttpRequest(httprequest) 41 | if httprequest.args.get('jsonp'): 42 | return JsonRequest(httprequest) 43 | if httprequest.mimetype in ("application/json", "application/json-rpc"): 44 | return JsonRequest(httprequest) 45 | else: 46 | return HttpRequest(httprequest) 47 | root.get_request = MethodType(get_request, root) 48 | 49 | origin_get_response = root.get_response 50 | def get_response(self, httprequest, result, explicit_session): 51 | response = origin_get_response(httprequest, result, explicit_session) 52 | if hasattr(response, 'headers') and response.headers.get('set-sid'): 53 | response.headers.set('set-sid', httprequest.session.sid) 54 | return response 55 | root.get_response = MethodType(get_response, root) 56 | 57 | -------------------------------------------------------------------------------- /ext_libs/weixin/lib/Sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ######################################################################### 4 | # Author: jonyqin 5 | # Created Time: Thu 11 Sep 2014 03:55:41 PM CST 6 | # File Name: demo.py 7 | # Description: WXBizMsgCrypt 使用demo文件 8 | ######################################################################### 9 | from WXBizMsgCrypt import WXBizMsgCrypt 10 | if __name__ == "__main__": 11 | """ 12 | 1.第三方回复加密消息给公众平台; 13 | 2.第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。 14 | """ 15 | encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" 16 | to_xml = """ 1407743423 """ 17 | token = "spamtest" 18 | nonce = "1320562132" 19 | appid = "wx2c2769f8efd9abc2" 20 | #测试加密接口 21 | encryp_test = WXBizMsgCrypt(token,encodingAESKey,appid) 22 | ret,encrypt_xml = encryp_test.EncryptMsg(to_xml,nonce) 23 | print(ret,encrypt_xml) 24 | 25 | 26 | #测试解密接口 27 | timestamp = "1409735669" 28 | msg_sign = "5d197aaffba7e9b25a30732f161a50dee96bd5fa" 29 | 30 | from_xml = """14097356686054768590064713728""" 31 | decrypt_test = WXBizMsgCrypt(token,encodingAESKey,appid) 32 | ret ,decryp_xml = decrypt_test.DecryptMsg(from_xml, msg_sign, timestamp, nonce) 33 | print(ret ,decryp_xml) 34 | -------------------------------------------------------------------------------- /security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | 3 | access_wxapp_banner_group_wxapp_config,wxapp_banner.group_wxapp_config,model_wxapp_banner,group_wxapp_config,1,1,1,1 4 | access_wxapp_confirm_all,wxapp_confirm.group_False,model_wxapp_confirm,,1,1,1,0 5 | access_wxapp_banner_group_wxapp_sale,wxapp_banner.group_wxapp_sale,model_wxapp_banner,group_wxapp_sale,1,1,1,1 6 | access_wxapp_config_group_wxapp_config,wxapp_config.group_wxapp_config,model_wxapp_config,group_wxapp_config,1,1,1,1 7 | access_wxapp_payment_group_wxapp_sale,wxapp_payment.group_wxapp_sale,model_wxapp_payment,group_wxapp_sale,1,0,0,0 8 | access_wxapp_product_category_group_wxapp_sale,wxapp_product_category.group_wxapp_sale,model_wxapp_product_category,group_wxapp_sale,1,1,1,1 9 | access_wxapp_product_category_group_wxapp_config,wxapp_product_category.group_wxapp_config,model_wxapp_product_category,group_wxapp_config,1,1,1,1 10 | access_wxapp_user_group_wxapp_sale,wxapp_user.group_wxapp_sale,model_wxapp_user,group_wxapp_sale,1,1,0,1 11 | access_oe_province_group_wxapp_config,oe_province.group_wxapp_config,model_oe_province,group_wxapp_config,1,1,1,1 12 | access_oe_province_group_wxapp_sale,oe_province.group_wxapp_sale,model_oe_province,group_wxapp_sale,1,0,0,0 13 | access_oe_city_group_wxapp_config,oe_city.group_wxapp_config,model_oe_city,group_wxapp_config,1,1,1,1 14 | access_oe_city_group_wxapp_sale,oe_city.group_wxapp_sale,model_oe_city,group_wxapp_sale,1,0,0,0 15 | access_oe_district_group_wxapp_config,oe_district.group_wxapp_config,model_oe_district,group_wxapp_config,1,1,1,1 16 | access_oe_district_group_wxapp_sale,oe_district.group_wxapp_sale,model_oe_district,group_wxapp_sale,1,0,0,0 17 | access_oe_shipper_group_wxapp_config,oe_shipper.group_wxapp_config,model_oe_shipper,group_wxapp_config,1,1,1,1 18 | access_oe_shipper_group_wxapp_sale,oe_shipper.group_wxapp_sale,model_oe_shipper,group_wxapp_sale,1,0,0,0 19 | access_wxapp_banner_all,wxapp_banner.group_False,model_wxapp_banner,,1,0,0,0 20 | access_product_template_all,product_template.group_False,account.model_product_template,,1,0,0,0 21 | access_wxapp_notice_group_wxapp_config,wxapp_notice.group_wxapp_config,model_wxapp_notice,group_wxapp_config,1,1,1,1 22 | access_wxapp_notice_group_wxapp_sale,wxapp_notice.group_wxapp_sale,model_wxapp_notice,group_wxapp_sale,1,1,1,1 23 | access_wxapp_product_category_all,wxapp_product_category.group_False,model_wxapp_product_category,,1,0,0,0 -------------------------------------------------------------------------------- /controllers/notice.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | 8 | from .. import defs 9 | from .base import BaseController, jsonapi 10 | from .base import convert_static_link 11 | 12 | import logging 13 | 14 | _logger = logging.getLogger(__name__) 15 | 16 | 17 | class WxappNotice(http.Controller, BaseController): 18 | 19 | @http.route('/wxa//notice/list', auth='public', methods=['GET', 'POST'], csrf=False) 20 | def list(self, sub_domain, pageSize=5, **kwargs): 21 | try: 22 | ret, entry = self._check_domain(sub_domain) 23 | if ret:return ret 24 | 25 | notices = request.env['wxapp.notice'].sudo().search([]) 26 | data = { 27 | 'dataList': [ 28 | {'id': e.id, 'title': e.title} for e in notices 29 | ] 30 | } 31 | 32 | return self.res_ok(data) 33 | 34 | except Exception as e: 35 | _logger.exception(e) 36 | return self.res_err(-1, str(e)) 37 | 38 | @http.route('/wxa//notice/last-one', auth='public', methods=['GET'], csrf=False) 39 | @jsonapi 40 | def last_one(self, sub_domain, id=False, **kwargs): 41 | ret, entry = self._check_domain(sub_domain) 42 | if ret:return ret 43 | 44 | _type = kwargs.get('type') 45 | last_notice = request.env['wxapp.notice'].sudo().search([], order='write_date desc', limit=1) 46 | if last_notice: 47 | data = { 48 | 'id': last_notice.id, 49 | 'title': last_notice.title, 50 | } 51 | return self.res_ok(data) 52 | else: 53 | return self.res_err(700) 54 | 55 | @http.route('/wxa//notice/detail', auth='public', methods=['GET'], csrf=False) 56 | def detail(self, sub_domain, id=False, **kwargs): 57 | try: 58 | ret, entry = self._check_domain(sub_domain) 59 | if ret:return ret 60 | 61 | notice = request.env['wxapp.notice'].sudo().browse(int(id)) 62 | data = { 63 | 'title': notice.title, 64 | 'content': convert_static_link(request, notice.content), 65 | } 66 | 67 | return self.res_ok(data) 68 | 69 | except Exception as e: 70 | _logger.exception(e) 71 | return self.res_err(-1, str(e)) 72 | -------------------------------------------------------------------------------- /controllers/product_category.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | from odoo import release 8 | 9 | from .. import defs 10 | from .base import BaseController 11 | 12 | 13 | import logging 14 | 15 | _logger = logging.getLogger(__name__) 16 | 17 | DEFAULT_IMG_URL = '/web/static/src/img/placeholder.png' 18 | odoo_ver = release.version_info[0] 19 | if odoo_ver>=15: 20 | DEFAULT_IMG_URL = '/web/static/img/placeholder.png' 21 | 22 | class WxappCategory(http.Controller, BaseController): 23 | 24 | def get_categorys(self, entry): 25 | all_category = request.env['wxapp.product.category'].sudo().search([ 26 | ('is_use', '=', True) 27 | ]) 28 | return all_category 29 | 30 | @http.route('/wxa//shop/goods/category/all', auth='public', methods=['GET']) 31 | def all(self, sub_domain): 32 | ret, entry = self._check_domain(sub_domain) 33 | if ret:return ret 34 | 35 | try: 36 | base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url') 37 | all_category = self.get_categorys(entry) 38 | if not all_category: 39 | return self.res_err(404) 40 | 41 | parent_cate = [e.pid.id for e in all_category] 42 | parent_cate = set(parent_cate) 43 | 44 | data = [ 45 | { 46 | "dateAdd": each_category.create_date, 47 | "dateUpdate": each_category.write_date, 48 | "icon": each_category.get_icon_image() if each_category.icon else '%s%s'%(base_url,DEFAULT_IMG_URL), 49 | "id": each_category.id, 50 | "isUse": each_category.is_use, 51 | "key": each_category.key, 52 | "level": each_category.level, 53 | "index_display": each_category.index_display, 54 | "name": each_category.name, 55 | "paixu": each_category.sort or 0, 56 | "pid": each_category.pid.id if each_category.pid else 0, 57 | "hasChild": each_category.id in parent_cate, 58 | "type": '', #each_category.category_type, 59 | "tag_id": hasattr(each_category, 'tag_id') and each_category.tag_id.id or '', 60 | "userId": each_category.create_uid.id 61 | } for each_category in all_category 62 | ] 63 | return self.res_ok(data) 64 | 65 | except Exception as e: 66 | _logger.exception(e) 67 | return self.res_err(-1, str(e)) 68 | -------------------------------------------------------------------------------- /controllers/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | 8 | from .base import BaseController 9 | 10 | 11 | import logging 12 | 13 | _logger = logging.getLogger(__name__) 14 | 15 | 16 | 17 | class WxappConfig(http.Controller, BaseController): 18 | 19 | @http.route('/wxa//config/get-value', auth='public', methods=['GET']) 20 | def get_value(self, sub_domain, key=None, **kwargs): 21 | try: 22 | ret, entry = self._check_domain(sub_domain) 23 | if ret:return ret 24 | 25 | if not key: 26 | return self.res_err(300) 27 | 28 | data = { 29 | 'dbname': request.env.cr.dbname, 30 | 'creatAt': entry.create_date, 31 | 'dateType': 0, 32 | 'id': entry.id, 33 | 'key': key, 34 | 'remark': '', 35 | 'updateAt': entry.write_date, 36 | 'userId': entry.id, 37 | 'value': entry.get_config(key) 38 | } 39 | data.update(entry.get_ext_config()) 40 | return self.res_ok(data) 41 | 42 | except AttributeError: 43 | return self.res_err(404) 44 | 45 | except Exception as e: 46 | _logger.exception(e) 47 | return self.res_err(-1, str(e)) 48 | 49 | @http.route('/wxa//config/values', auth='public', methods=['GET']) 50 | def get_values(self, sub_domain, keys=None, **kwargs): 51 | keys = keys.split(',') 52 | try: 53 | ret, entry = self._check_domain(sub_domain) 54 | if ret:return ret 55 | 56 | if not keys: 57 | return self.res_err(300) 58 | 59 | data = [] 60 | for key in keys: 61 | key = key.strip() 62 | data.append({'key': key, 'value': entry.get_config(key)}) 63 | data.append({'key': 'dbname', 'value': request.env.cr.dbname}) 64 | ext_config = entry.get_ext_config() 65 | for key in ext_config: 66 | data.append({'key': key, 'value': ext_config[key]}) 67 | 68 | return self.res_ok(data) 69 | 70 | except Exception as e: 71 | _logger.exception(e) 72 | return self.res_err(-1, str(e)) 73 | 74 | @http.route('/wxa//config/vipLevel', auth='public', methods=['GET']) 75 | def get_viplevel(self, sub_domain, key=None, **kwargs): 76 | try: 77 | ret, entry = self._check_domain(sub_domain) 78 | if ret:return ret 79 | 80 | return self.res_ok(entry.get_level()) 81 | 82 | except Exception as e: 83 | _logger.exception(e) 84 | return self.res_err(-1, str(e)) 85 | -------------------------------------------------------------------------------- /views/wxapp_config_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wxapp.config.view_tree 7 | wxapp.config 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | wxapp.config.view_form 21 | wxapp.config 22 | form 23 | 999 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 电商设置 46 | wxapp.config 47 | form 48 | tree,form 49 | current 50 | 1 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /views/wxapp_product_category_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wxapp.product.category.view_tree 7 | wxapp.product.category 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | wxapp.product.category.view_form 24 | wxapp.product.category 25 | form 26 | 999 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 商品分类 47 | wxapp.product.category 48 | form 49 | tree,form 50 | current 51 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /views/wxapp_banner_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wxapp.banner.view_tree 7 | wxapp.banner 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | wxapp.banner.view_form 24 | wxapp.banner 25 | form 26 | 999 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 横幅 48 | wxapp.banner 49 | form 50 | tree,form 51 | current 52 | 53 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /models/wxapp_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models, fields, api 4 | 5 | from .. import defs 6 | 7 | 8 | class WxappUser(models.Model): 9 | 10 | _name = 'wxapp.user' 11 | _description = u'微信客户' 12 | _inherits = {'res.partner': 'partner_id'} 13 | _order = 'id desc' 14 | 15 | name = fields.Char(related='partner_id.name',string='名称', inherited=True) 16 | nickname = fields.Char('昵称') 17 | 18 | open_id = fields.Char('OpenId', index=True, readonly=True) 19 | union_id = fields.Char('UnionId', readonly=True) 20 | gender = fields.Integer('gender') 21 | language = fields.Char('语言') 22 | phone = fields.Char('手机号码') 23 | country = fields.Char('国家') 24 | province = fields.Char('省份') 25 | city = fields.Char('城市') 26 | avatar = fields.Html('头像', compute='_compute_avatar') 27 | avatar_url = fields.Char('头像链接') 28 | register_ip = fields.Char('注册IP') 29 | last_login = fields.Datetime('登陆时间') 30 | ip = fields.Char('登陆IP') 31 | status = fields.Selection(defs.WechatUserStatus.attrs.items(), string='状态', default=defs.WechatUserStatus.default) 32 | register_type = fields.Selection(defs.WechatUserRegisterType.attrs.items(), string='注册来源', default=defs.WechatUserRegisterType.app) 33 | 34 | partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', string='关联联系人', auto_join=True) # 35 | address_ids = fields.One2many('res.partner', compute='_compute_address_ids', string='收货地址') 36 | entry_id = fields.Integer('来源ID') 37 | 38 | _sql_constraints = [( 39 | 'wxapp_user_union_id_unique', 40 | 'UNIQUE (union_id, create_uid)', 41 | 'wechat user union_id with create_uid is existed!' 42 | ), 43 | ( 44 | 'wxapp_user_open_id_unique', 45 | 'UNIQUE (open_id, create_uid)', 46 | 'wechat user open_id with create_uid is existed!' 47 | ), 48 | ] 49 | 50 | @api.multi 51 | def action_created(self, data=None): 52 | pass 53 | 54 | @api.multi 55 | @api.depends('avatar_url') 56 | def _compute_avatar(self): 57 | for each_record in self: 58 | if each_record.avatar_url: 59 | each_record.avatar = """ 60 | 61 | """.format(avatar_url=each_record.avatar_url) 62 | else: 63 | each_record.avatar = '' 64 | 65 | @api.depends('partner_id') 66 | def _compute_address_ids(self): 67 | for obj in self: 68 | obj.address_ids = obj.partner_id.child_ids.filtered(lambda r: r.type == 'delivery') 69 | 70 | def bind_mobile(self, mobile): 71 | vals = {'mobile': mobile} 72 | if self.partner_id.name=='微信用户': 73 | vals['name'] = mobile 74 | self.partner_id.write(vals) 75 | 76 | def check_account_ok(self): 77 | return True 78 | 79 | def get_balance(self): 80 | return hasattr(self, 'balance') and self.balance or 0 81 | 82 | def get_score(self): 83 | return hasattr(self, 'score') and self.score or 0 84 | 85 | def get_credit_limit(self): 86 | return 0 87 | -------------------------------------------------------------------------------- /models/oe_district.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import json 4 | 5 | from odoo import models, fields, api 6 | 7 | _logger = logging.getLogger(__name__) 8 | 9 | 10 | class District(models.Model): 11 | 12 | _name = 'oe.district' 13 | _description = u'区' 14 | 15 | pid = fields.Many2one('oe.city', string='城市') 16 | name = fields.Char('名称', requried=True) 17 | 18 | @api.model_cr 19 | def _register_hook(self): 20 | """ stuff to do right after the registry is built """ 21 | _logger.info('>>> registry hook...') 22 | #from ..data import province_city_district_data 23 | #self.env.cr.execute(province_city_district_data) 24 | 25 | @api.model_cr 26 | def init(self): 27 | _logger.info('>>> init...') 28 | from ..data.oe_district_full_datas import init_json 29 | objs = json.loads(init_json) 30 | district_sql = city_sql = province_sql = '' 31 | name_key = 'value' 32 | children_key = 'children' 33 | for province in objs: 34 | province_sql += """INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (%s, '%s', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING;\n"""%(province['code'], province[name_key]) 35 | city0code = province[children_key][0]['code'] 36 | if city0code[-2:]=='00': 37 | for city in province[children_key]: 38 | city_sql += """INSERT INTO oe_city (id, pid, name, create_uid, create_date, write_uid, write_date) VALUES (%s, %s, '%s', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING;\n"""%(city['code'], province['code'], city[name_key]) 39 | # _logger.info('>>> load city %s', city) 40 | for district in city[children_key]: 41 | district_sql += """INSERT INTO oe_district (id, pid, name, create_uid, create_date, write_uid, write_date) VALUES (%s, %s, '%s', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING;"""%(district['code'], city['code'], district[name_key]) 42 | else: 43 | # province 为直辖市 44 | city_code = '%s0100'%province['code'][:2] 45 | city_sql += """INSERT INTO oe_city (id, pid, name, create_uid, create_date, write_uid, write_date) VALUES (%s, %s, '%s', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING;\n"""%(city_code, province['code'], province[name_key]) 46 | for district in province[children_key]: 47 | district_sql += """INSERT INTO oe_district (id, pid, name, create_uid, create_date, write_uid, write_date) VALUES (%s, %s, '%s', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING;"""%(district['code'], city_code, district[name_key]) 48 | 49 | self.env.cr.execute(province_sql) 50 | self.env.cr.execute("select setval('oe_province_id_seq', max(id)) from oe_province;") 51 | self.env.cr.execute(city_sql) 52 | self.env.cr.execute("select setval('oe_city_id_seq', max(id)) from oe_city;") 53 | self.env.cr.execute(district_sql) 54 | self.env.cr.execute("select setval('oe_district_id_seq', max(id)) from oe_district;") 55 | -------------------------------------------------------------------------------- /views/wxapp_payment_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wxapp.payment.view_tree 7 | wxapp.payment 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | wxapp.payment.view_form 26 | wxapp.payment 27 | form 28 | 999 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 支付记录 59 | wxapp.payment 60 | form 61 | tree,form 62 | current 63 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /ext_libs/weixin/msg_template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | TEXT_TEMPLATE = """ 5 | 6 | 7 | 8 | {create_time} 9 | 10 | 11 | 12 | """ 13 | 14 | IMAGE_TEMPLATE = """ 15 | 16 | 17 | 18 | {create_time} 19 | 20 | 21 | 22 | 23 | 24 | """ 25 | 26 | VOICE_TEMPLATE = """ 27 | 28 | 29 | 30 | {create_time} 31 | 32 | 33 | 34 | 35 | 36 | """ 37 | 38 | VIDEO_TEMPLATE = """ 39 | 40 | 41 | 42 | {create_time} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | """ 51 | 52 | THUM_MUSIC_TEMPLATE = """ 53 | 54 | 55 | 56 | {create_time} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | 68 | NOTHUM_MUSIC_TEMPLATE = """ 69 | 70 | 71 | 72 | {create_time} 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | """ 82 | 83 | ARITICLE_TEMPLATE = """ 84 | 85 | 86 | 87 | {create_time} 88 | 89 | {count} 90 | {items} 91 | 92 | """ 93 | 94 | ARITICLE_ITEM_TEMPLATE = """ 95 | 96 | 97 | 98 | 99 | 100 | 101 | """ 102 | -------------------------------------------------------------------------------- /controllers/banner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | 8 | from .. import defs 9 | from .base import BaseController 10 | 11 | import logging 12 | 13 | _logger = logging.getLogger(__name__) 14 | 15 | 16 | class WxappBanner(http.Controller, BaseController): 17 | 18 | @http.route('/wxa//banner/list', auth='public', methods=['GET']) 19 | def list(self, sub_domain, default_banner=True, **kwargs): 20 | _logger.info('>>> banner_list %s %s', default_banner, kwargs) 21 | banner_type = kwargs.get('type') 22 | try: 23 | ret, entry = self._check_domain(sub_domain) 24 | if ret:return ret 25 | 26 | if banner_type=='new': 27 | banner_type = 'index' 28 | domain = [('status', '=', True)] 29 | if banner_type=='index': 30 | domain.append(('ptype', '=like', banner_type + '%')) 31 | else: 32 | domain.append(('ptype', '=', banner_type)) 33 | banner_list = request.env['wxapp.banner'].sudo().search(domain) 34 | 35 | data = [] 36 | if banner_list: 37 | data = [ 38 | { 39 | "businessId": each_banner.get_business_id(), 40 | "dateAdd": each_banner.create_date, 41 | "dateUpdate": each_banner.write_date, 42 | "id": each_banner.id, 43 | "linkType": each_banner.link_type, 44 | "linkPage": each_banner.link_page, 45 | "linkUrl": each_banner.link_url or '', 46 | "paixu": each_banner.sort or 0, 47 | "picUrl": each_banner.get_main_image(), 48 | "remark": each_banner.remark or '', 49 | "status": 0 if each_banner.status else 1, 50 | "statusStr": defs.BannerStatus.attrs[each_banner.status], 51 | "title": each_banner.title, 52 | "type": each_banner.ptype, 53 | "userId": each_banner.create_uid.id 54 | } for each_banner in banner_list 55 | ] 56 | if banner_type=='app': 57 | if len(data)<1: 58 | return self.res_err(700) 59 | 60 | if 0 and banner_type=='index': 61 | recommend_goods = request.env(user=1)['product.template'].search([ 62 | ('recommend_status', '=', True), 63 | ('wxapp_published', '=', True) 64 | ], limit=5) 65 | 66 | data += [ 67 | { 68 | "goods": True, 69 | "businessId": goods.id, 70 | "dateAdd": goods.create_date, 71 | "dateUpdate": goods.write_date, 72 | "id": goods.id, 73 | "linkUrl": '', 74 | "paixu": goods.sequence or 0, 75 | "picUrl": goods.main_img, 76 | "remark": '', 77 | "status": 0 if goods.wxapp_published else 1, 78 | "statusStr": '', 79 | "title": goods.name, 80 | "type": 0, 81 | "userId": goods.create_uid.id 82 | } for goods in recommend_goods 83 | ] 84 | 85 | if not data: 86 | return self.res_err(700) 87 | 88 | return self.res_ok(data) 89 | 90 | except Exception as e: 91 | _logger.exception(e) 92 | return self.res_err(-1, str(e)) 93 | -------------------------------------------------------------------------------- /models/product.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | import json 4 | 5 | from odoo import models, fields, api 6 | 7 | from .. import defs 8 | 9 | _logger = logging.getLogger(__name__) 10 | 11 | class ProductTemplate(models.Model): 12 | 13 | _inherit = "product.template" 14 | 15 | wxpp_category_id = fields.Many2one('wxapp.product.category', string='电商分类', ondelete='set null') 16 | characteristic = fields.Text('商品特色') 17 | recommend_status = fields.Boolean('是否推荐') 18 | wxapp_published = fields.Boolean('是否上架', default=True) 19 | description_wxapp = fields.Html('商品描述') 20 | original_price = fields.Float('原始价格', default=0) 21 | qty_public_tpl = fields.Integer('库存', default=0) 22 | qty_show = fields.Integer('库存数量', compute='_compute_qty_show') 23 | 24 | number_good_reputation = fields.Integer('好评数', default=0) 25 | number_fav = fields.Integer('收藏数', default=0) 26 | views = fields.Integer('浏览量', default=0) 27 | main_img = fields.Char('主图', compute='_get_main_image') 28 | images_data = fields.Char('图片', compute='_get_multi_images') 29 | 30 | 31 | def _get_main_image(self): 32 | _logger.info('>>> _get_main_image') 33 | base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') 34 | for obj in self: 35 | obj.main_img = '%s/web/image/product.template/%s/image_256/'%(base_url, obj.id) 36 | 37 | def _get_multi_images(self): 38 | _logger.info('>>> _get_multi_images') 39 | base_url=self.env['ir.config_parameter'].sudo().get_param('web.base.url') 40 | for product in self: 41 | _list = [] 42 | if hasattr(product, 'product_template_image_ids'): 43 | for obj in product.product_template_image_ids: 44 | _dict = { 45 | "id": obj.id, 46 | "goodsId": product.id, 47 | "pic": '%s/web/image/product.image/%s/image_1024/'%(base_url, obj.id) 48 | } 49 | _list.append(_dict) 50 | _list.append({ 51 | 'id': product.id, 52 | 'goodsId': product.id, 53 | 'pic': '%s/web/image/product.template/%s/image_1024/'%(base_url, product.id) 54 | }) 55 | product.images_data = json.dumps(_list) 56 | 57 | def batch_get_main_image(self): 58 | self._get_main_image() 59 | 60 | def get_present_qty(self): 61 | return self.qty_public_tpl 62 | 63 | def get_qty(self): 64 | return self.qty_public_tpl 65 | 66 | def _compute_qty_show(self): 67 | for obj in self: 68 | obj.qty_show = obj.get_present_qty() 69 | 70 | def change_qty(self, val): 71 | self.write({'qty_public_tpl': self.qty_public_tpl + val}) 72 | 73 | def get_present_price(self, quantity=1): 74 | return self.list_price 75 | 76 | @api.model 77 | def cli_price(self, price): 78 | return round(price, 2) 79 | 80 | class ProductProduct(models.Model): 81 | 82 | _inherit = "product.product" 83 | 84 | present_price = fields.Float('现价', default=0, digits=defs.get_precision()) 85 | qty_public = fields.Integer('库存', default=0, required=True) 86 | attr_val_str = fields.Char('规格', compute='_compute_attr_val_str', store=True, default='') 87 | 88 | @api.multi 89 | @api.depends('product_template_attribute_value_ids') 90 | def _compute_attr_val_str(self): 91 | for obj in self: 92 | obj.attr_val_str = '' 93 | 94 | 95 | def get_property_str(self): 96 | return '' 97 | 98 | def get_present_price(self, quantity=1): 99 | return self.present_price or self.lst_price or self.product_tmpl_id.list_price 100 | 101 | def get_present_qty(self): 102 | return self.qty_public 103 | 104 | def get_qty(self): 105 | return self.qty_public 106 | 107 | def change_qty(self, val): 108 | self.write({'qty_public': self.qty_public + val}) 109 | -------------------------------------------------------------------------------- /views/wxapp_user_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wxapp.user.view_tree 7 | wxapp.user 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | wxapp.user.view_form 32 | wxapp.user 33 | form 34 | 999 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | wxapp_user_filter 68 | wxapp.user 69 | search 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 微信客户 78 | wxapp.user 79 | form 80 | tree,form 81 | current 82 | 83 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /models/wxapp_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from odoo import models, fields, api 5 | 6 | _logger = logging.getLogger(__name__) 7 | 8 | class Platform(object): 9 | 10 | def __init__(self, val): 11 | self.val = val 12 | 13 | def __get__(self, obj, objtype): 14 | return self.val 15 | 16 | def __set__(self, obj, val): 17 | self.val = val 18 | 19 | 20 | class WxappConfig(models.Model): 21 | 22 | _name = 'wxapp.config' 23 | _description = u'对接设置' 24 | _rec_name = 'mall_name' 25 | _platform = Platform('wxapp') 26 | 27 | sub_domain = fields.Char('接口前缀', help='商城访问的接口url前缀', index=True, required=True, default='p') 28 | 29 | mall_name = fields.Char('商城名称', help='显示在顶部') 30 | 31 | app_id = fields.Char('AppId') 32 | secret = fields.Char('Secret') 33 | 34 | team_id = fields.Many2one('crm.team', string='所属销售渠道', required=True) 35 | gmt_diff = fields.Integer('客户端时区GMT ± N', default=8) 36 | 37 | def need_login(self): 38 | return False 39 | 40 | def get_config(self, key): 41 | if key=='mallName': 42 | key = 'mall_name' 43 | if hasattr(self, key): 44 | return self.__getattribute__(key) 45 | else: 46 | return None 47 | 48 | @api.model 49 | def get_entry(self, sub_domain): 50 | # mirror 默认使用平台的配置 51 | if sub_domain in ['mirror']: 52 | entry = self.env.ref('oejia_weshop.wxapp_config_data_1') 53 | entry._platform = sub_domain 54 | return entry 55 | config = self.search([('sub_domain', '=', sub_domain)]) 56 | if config: 57 | config.ensure_one() 58 | config._platform = 'wxapp|%s' % config.id 59 | return config 60 | else: 61 | return False 62 | 63 | def get_id(self): 64 | if self._platform in ['mirror']: 65 | return self.id 66 | else: 67 | return int(self._platform.replace('wxapp|', '')) 68 | 69 | def get_company_id(self): 70 | return False 71 | 72 | @api.model 73 | def get_from_team(self, team_id): 74 | config = self.search([('team_id', '=', team_id)]) 75 | if config: 76 | config.ensure_one() 77 | return config 78 | else: 79 | return False 80 | 81 | @api.model 82 | def get_from_id(self, id): 83 | return self.browse(id) 84 | 85 | @api.multi 86 | def clean_all_token(self): 87 | self.env['wxapp.access_token'].search([]).unlink() 88 | 89 | @api.multi 90 | def clean_all_token_window(self): 91 | new_context = dict(self._context) or {} 92 | new_context['default_info'] = "确认将所有会话 token 清除?" 93 | new_context['default_model'] = 'wxapp.config' 94 | new_context['default_method'] = 'clean_all_token' 95 | new_context['record_ids'] = [obj.id for obj in self] 96 | return { 97 | 'name': u'确认清除', 98 | 'type': 'ir.actions.act_window', 99 | 'res_model': 'wxapp.confirm', 100 | 'res_id': None, 101 | 'view_mode': 'form', 102 | 'view_type': 'form', 103 | 'context': new_context, 104 | 'view_id': self.env.ref('oejia_weshop.confirm_view_form').id, 105 | 'target': 'new' 106 | } 107 | 108 | def get_config_view(self): 109 | new_context = dict(self._context) or {} 110 | return { 111 | 'name': u'设置', 112 | 'type': 'ir.actions.act_window', 113 | 'res_model': self._name, 114 | 'res_id': self.env.ref('oejia_weshop.wxapp_config_data_1').id, 115 | 'view_mode': 'form', 116 | 'view_type': 'form', 117 | 'context': new_context, 118 | 'view_id': self.env.ref('oejia_weshop.wxapp_config_view_form_1003').id, 119 | 'target': 'current' 120 | } 121 | 122 | def get_level(self): 123 | return 0 124 | 125 | def get_ext_config(self): 126 | return {} 127 | -------------------------------------------------------------------------------- /ext_libs/weixin/response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from .reply import TextReply 5 | from .config import AUTO_REPLY_CONTENT 6 | 7 | 8 | ALLOWED_MSG_TYPES = set(['text', 'image', 'voice', 'video', 9 | 'shortvideo', 'location', 'link']) 10 | ALLOWED_EVENTS = set(['subscribe', 'unsubscribe', 'unsub_scan', 11 | 'scan', 'click', 'location', 'view', 12 | 'templatesendjobfinish']) 13 | 14 | 15 | class WXResponse(object): 16 | 17 | auto_reply_content = AUTO_REPLY_CONTENT 18 | 19 | def __init__(self, xml_dict): 20 | # 微信请求的数据 21 | self.data = xml_dict.get('xml') or xml_dict 22 | self.reply_params = {} 23 | self.reply = None 24 | 25 | def __call__(self): 26 | # make response 27 | return self.make_response() 28 | 29 | def check_event(self): 30 | ''' 31 | 接收的事件 32 | subscribe:订阅 33 | unsubscribe:取消订阅 34 | subscribe+EventKey+Ticket:用户未关注时,进行关注后的事件 35 | SCAN:用户已关注时扫描二维码 36 | LOCATION: 上报地理位置 37 | CLICK: 点击菜单 38 | VIEW: 点击菜单链接 39 | ''' 40 | event = self.data.get('Event') 41 | if not event: 42 | return None 43 | if event == 'subscribe': 44 | if self.data.get('EventKey') and self.data.get('Ticket'): 45 | return 'unsub_scan' 46 | return event 47 | return event.lower() 48 | 49 | def _subscribe_event_handler(self): 50 | # 订阅事件处理逻辑 51 | self.reply_params['content'] = self.auto_reply_content 52 | self.reply = TextReply(**self.reply_params).render() 53 | 54 | def _unsubscribe_event_handler(self): 55 | # 取消订阅事件处理逻辑 56 | pass 57 | 58 | def _unsub_scan_event_handler(self): 59 | # 扫描二维码 用户未关注时,进行关注后的事件 60 | pass 61 | 62 | def _scan_event_handler(self): 63 | # 用户已关注时扫描二维码 64 | pass 65 | 66 | def _click_event_handler(self): 67 | # 点击菜单事件的逻辑 68 | pass 69 | 70 | def _location_event_handler(self): 71 | # 上报地理位置的处理逻辑 72 | pass 73 | 74 | def _view_event_handler(self): 75 | # 点击菜单链接的逻辑 76 | pass 77 | 78 | def _templatesendjobfinish_event_handler(self): 79 | # 模板消息推送完成逻辑 80 | pass 81 | 82 | def _text_msg_handler(self): 83 | # 文字消息处理逻辑 84 | self.reply_params['content'] = self.auto_reply_content 85 | self.reply = TextReply(**self.reply_params).render() 86 | 87 | def _image_msg_handler(self): 88 | # 图片消息处理逻辑 89 | pass 90 | 91 | def _voice_msg_handler(self): 92 | # 语音消息处理逻辑 93 | pass 94 | 95 | def _video_msg_handler(self): 96 | # 视频消息处理逻辑 97 | pass 98 | 99 | def _shortvideo_msg_handler(self): 100 | # 小视频消息处理逻辑 101 | pass 102 | 103 | def _location_msg_handler(self): 104 | # 地理位置消息处理逻辑 105 | pass 106 | 107 | def _link_msg_handler(self): 108 | # 链接消息处理逻辑 109 | pass 110 | 111 | def _data_handler(self): 112 | # 只取出消息类型和事件 113 | msg_type = self.data.get('MsgType') 114 | self.reply_params['to_user'] = self.data.get('FromUserName') 115 | self.reply_params['from_user'] = self.data.get('ToUserName') 116 | event = None 117 | if msg_type == 'event': 118 | event = self.check_event() 119 | return msg_type, event 120 | 121 | def _event_handler(self, event): 122 | if event not in ALLOWED_EVENTS: 123 | # TODO raise except 124 | return 125 | methodname = '_{0}_event_handler'.format(event) 126 | method = getattr(self, methodname, None) 127 | if method: 128 | return method() 129 | return 130 | 131 | def handler(self): 132 | msg_type, event = self._data_handler() 133 | if msg_type == 'event': 134 | return self._event_handler(event) 135 | elif msg_type in ALLOWED_MSG_TYPES: 136 | methodname = '_{0}_msg_handler'.format(msg_type) 137 | return getattr(self, methodname, None)() 138 | else: 139 | # TODO raise except 140 | pass 141 | 142 | def make_response(self): 143 | """ 144 | :param reply: WXReply.render(**args) 145 | """ 146 | self.handler() 147 | if not self.reply: 148 | return 'success' 149 | return self.reply 150 | -------------------------------------------------------------------------------- /views/product_template_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | product.template.tree.wxapp_inherit 7 | product.template 8 | 9 | tree 10 | 11 | 12 | 13 | sequence 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | product.template.search.wxapp_inherit 26 | product.template 27 | 28 | search 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | product.template.form.wxapp_inherit 41 | product.template 42 | 43 | form 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 批量上架 69 | 70 | True 71 | ir.actions.server 72 | 73 | 74 | code 75 | records.write({'wxapp_published': True}) 76 | 77 | 78 | 批量下架 79 | 80 | True 81 | ir.actions.server 82 | 83 | 84 | code 85 | records.write({'wxapp_published': False}) 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /views/sale_order_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | sale.order.view_form 7 | sale.order 8 | form 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | sale.order.form_weshop_inherit 28 | sale.order 29 | 30 | form 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | sale.order.search.inherit_wxapp.quotation 47 | sale.order 48 | 49 | search 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | sale.order.tree.weshop 58 | sale.order 59 | tree 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 电商订单 77 | sale.order 78 | form 79 | tree,kanban,form,calendar,pivot,graph 80 | current 81 | 82 | {'hide_sale': True} 83 | 84 | 85 | 暂无订单 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /models/sale_order.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from odoo import models, fields, api 4 | 5 | from .. import defs 6 | 7 | 8 | class SaleOrder(models.Model): 9 | 10 | _inherit = 'sale.order' 11 | 12 | customer_status = fields.Selection(defs.OrderStatus.attrs.items(), default=defs.OrderStatus.unpaid, 13 | required=True, string='状态', track_visibility='onchange', copy=False) 14 | 15 | number_goods = fields.Integer('商品数量', default=0) 16 | goods_price = fields.Float('商品总金额', requried=True, default=0, compute='_compute_pay_total', store=True) 17 | logistics_price = fields.Float('物流费用', requried=True, default=0) 18 | total = fields.Float('实际支付', requried=True, default=0, track_visibility='onchange', compute='_compute_pay_total', store=True, copy=False) 19 | 20 | province_id = fields.Many2one('oe.province', string='省') 21 | city_id = fields.Many2one('oe.city', string='市') 22 | district_id = fields.Many2one('oe.district', string='区') 23 | address = fields.Char('详细地址') 24 | full_address = fields.Char('联系人地址', compute='_compute_full_address', store=True) 25 | 26 | linkman = fields.Char('联系人') 27 | mobile = fields.Char('手机号码') 28 | zipcode = fields.Char('邮编', requried=True) 29 | 30 | delivery_time = fields.Datetime('发货时间') 31 | shipper_id = fields.Many2one('oe.shipper', string='承运商', track_visibility='onchange', copy=False) 32 | shipper_no = fields.Char('运单号', track_visibility='onchange', copy=False) 33 | shipper_traces = fields.Text('物流信息', copy=False) 34 | 35 | is_paid = fields.Boolean('已支付', default=False) #在线支付或余额支付 36 | 37 | 38 | 39 | @api.depends('province_id', 'city_id', 'district_id', 'address') 40 | def _compute_full_address(self): 41 | for order in self: 42 | order.full_address = u'{province_name} {city_name} {district_name} {address}'.format( 43 | province_name=order.province_id.name, 44 | city_name=order.city_id.name, 45 | district_name=order.district_id.name or '', 46 | address=order.address 47 | ) 48 | 49 | @api.depends('shipper_id', 'shipper_no') 50 | def _compute_traces(self): 51 | pass 52 | 53 | def get_traces(self, refresh=False): 54 | return self.shipper_traces 55 | 56 | @api.depends('logistics_price', 'amount_total') 57 | def _compute_pay_total(self): 58 | for order in self: 59 | order.total = order.amount_total 60 | order.goods_price = order.amount_total - order.logistics_price 61 | 62 | 63 | @api.multi 64 | def write(self, vals): 65 | result = super(SaleOrder, self).write(vals) 66 | if 'shipper_id' in vals or 'picked_code' in vals: 67 | self.delivery() 68 | return result 69 | 70 | @api.multi 71 | def delivery(self): 72 | self.write({'customer_status': 'unconfirmed', 'delivery_time': fields.Datetime.now()}) 73 | return True 74 | 75 | @api.multi 76 | def close_dialog(self): 77 | return {'type': 'ir.actions.act_window_close'} 78 | 79 | @api.multi 80 | def delivery_window(self): 81 | self.ensure_one() 82 | return { 83 | 'name': '送货', 84 | 'type': 'ir.actions.act_window', 85 | 'res_model': 'sale.order', 86 | 'res_id': self.id, 87 | 'view_mode': 'form', 88 | 'view_type': 'form', 89 | 'view_id': self.env.ref('oejia_weshop.sale_order_view_form_1029').id, 90 | 'target': 'new', 91 | 'domain': [], 92 | 'context': { 93 | 'default_customer_status': 'unconfirmed' 94 | } 95 | } 96 | 97 | @api.multi 98 | def action_paid(self): 99 | ''' 100 | 将订单置为已支付(电商) 101 | ''' 102 | self.write({'customer_status': 'pending', 'is_paid': True}) 103 | 104 | @api.multi 105 | def action_created(self, data=None): 106 | pass 107 | 108 | @api.multi 109 | def check_pay_window(self): 110 | new_context = dict(self._context) or {} 111 | new_context['default_info'] = "此订单客户尚未在线支付,确认将其变为已支付状态?" 112 | new_context['default_model'] = 'sale.order' 113 | new_context['default_method'] = 'action_paid' 114 | new_context['record_ids'] = [obj.id for obj in self] 115 | return { 116 | 'name': u'确认订单已支付', 117 | 'type': 'ir.actions.act_window', 118 | 'res_model': 'wxapp.confirm', 119 | 'res_id': None, 120 | 'view_mode': 'form', 121 | 'view_type': 'form', 122 | 'context': new_context, 123 | 'view_id': self.env.ref('oejia_weshop.confirm_view_form').id, 124 | 'target': 'new' 125 | } 126 | 127 | 128 | @api.multi 129 | def action_cancel(self): 130 | result = super(SaleOrder, self).action_cancel() 131 | self.write({'customer_status': 'closed'}) 132 | return result 133 | 134 | @api.multi 135 | def action_draft(self): 136 | result = super(SaleOrder, self).action_draft() 137 | self.write({'customer_status': 'unpaid'}) 138 | return result 139 | 140 | @api.multi 141 | def action_receive(self): 142 | self.write({'customer_status': 'unevaluated'}) 143 | 144 | def get_detail_ext(self, data): 145 | pass 146 | 147 | @api.multi 148 | def action_accounted(self, data=None): 149 | pass -------------------------------------------------------------------------------- /data/oe_province_datas.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | init_sql = """ 4 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (110000, '北京市', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 5 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (120000, '天津市', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 6 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (130000, '河北省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 7 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (140000, '山西省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 8 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (150000, '内蒙古自治区', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 9 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (210000, '辽宁省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 10 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (220000, '吉林省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 11 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (230000, '黑龙江省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 12 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (310000, '上海市', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 13 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (320000, '江苏省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 14 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (330000, '浙江省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 15 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (340000, '安徽省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 16 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (350000, '福建省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 17 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (360000, '江西省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 18 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (370000, '山东省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 19 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (410000, '河南省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 20 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (420000, '湖北省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 21 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (430000, '湖南省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 22 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (440000, '广东省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 23 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (450000, '广西壮族自治区', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 24 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (460000, '海南省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 25 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (500000, '重庆市', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 26 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (510000, '四川省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 27 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (520000, '贵州省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 28 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (530000, '云南省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 29 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (540000, '西藏自治区', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 30 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (610000, '陕西省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 31 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (620000, '甘肃省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 32 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (630000, '青海省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 33 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (640000, '宁夏回族自治区', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 34 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (650000, '新疆维吾尔自治区', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 35 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (710000, '台湾省', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 36 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (810000, '香港特别行政区', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 37 | INSERT INTO oe_province (id, name, create_uid, create_date, write_uid, write_date) VALUES (820000, '澳门特别行政区', 1, NOW() AT TIME ZONE 'UTC', 1, NOW() AT TIME ZONE 'UTC') ON CONFLICT DO NOTHING; 38 | """ 39 | -------------------------------------------------------------------------------- /const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | import warnings 4 | from types import GeneratorType 5 | 6 | import six 7 | 8 | 9 | class SortedDict(dict): 10 | """ 11 | A dictionary that keeps its keys in the order in which they're inserted. 12 | """ 13 | 14 | def __new__(cls, *args, **kwargs): 15 | instance = super(SortedDict, cls).__new__(cls, *args, **kwargs) 16 | instance.keyOrder = [] 17 | return instance 18 | 19 | def __init__(self, data=None): 20 | if data is None: 21 | data = {} 22 | elif isinstance(data, GeneratorType): 23 | # Unfortunately we need to be able to read a generator twice. Once 24 | # to get the data into self with our super().__init__ call and a 25 | # second time to setup keyOrder correctly 26 | data = list(data) 27 | super(SortedDict, self).__init__(data) 28 | if isinstance(data, dict): 29 | self.keyOrder = list(data) 30 | else: 31 | self.keyOrder = [] 32 | seen = set() 33 | for key, value in data: 34 | if key not in seen: 35 | self.keyOrder.append(key) 36 | seen.add(key) 37 | 38 | def __deepcopy__(self, memo): 39 | return self.__class__([(key, copy.deepcopy(value, memo)) 40 | for key, value in self.iteritems()]) 41 | 42 | def __copy__(self): 43 | # The Python's default copy implementation will alter the state 44 | # of self. The reason for this seems complex but is likely related to 45 | # subclassing dict. 46 | return self.copy() 47 | 48 | def __setitem__(self, key, value): 49 | if key not in self: 50 | self.keyOrder.append(key) 51 | super(SortedDict, self).__setitem__(key, value) 52 | 53 | def __delitem__(self, key): 54 | super(SortedDict, self).__delitem__(key) 55 | self.keyOrder.remove(key) 56 | 57 | def __iter__(self): 58 | return iter(self.keyOrder) 59 | 60 | def pop(self, k, *args): 61 | result = super(SortedDict, self).pop(k, *args) 62 | try: 63 | self.keyOrder.remove(k) 64 | except ValueError: 65 | # Key wasn't in the dictionary in the first place. No problem. 66 | pass 67 | return result 68 | 69 | def popitem(self): 70 | result = super(SortedDict, self).popitem() 71 | self.keyOrder.remove(result[0]) 72 | return result 73 | 74 | def _iteritems(self): 75 | for key in self.keyOrder: 76 | yield key, self[key] 77 | 78 | def _iterkeys(self): 79 | for key in self.keyOrder: 80 | yield key 81 | 82 | def _itervalues(self): 83 | for key in self.keyOrder: 84 | yield self[key] 85 | 86 | iteritems = _iteritems 87 | iterkeys = _iterkeys 88 | itervalues = _itervalues 89 | 90 | def items(self): 91 | return list(self.iteritems()) 92 | 93 | def keys(self): 94 | return list(self.iterkeys()) 95 | 96 | def values(self): 97 | return list(self.itervalues()) 98 | 99 | def update(self, dict_): 100 | for k, v in six.iteritems(dict_): 101 | self[k] = v 102 | 103 | def setdefault(self, key, default): 104 | if key not in self: 105 | self.keyOrder.append(key) 106 | return super(SortedDict, self).setdefault(key, default) 107 | 108 | def value_for_index(self, index): 109 | """Returns the value of the item at the given zero-based index.""" 110 | # This, and insert() are deprecated because they cannot be implemented 111 | # using collections.OrderedDict (Python 2.7 and up), which we'll 112 | # eventually switch to 113 | warnings.warn( 114 | "SortedDict.value_for_index is deprecated", PendingDeprecationWarning, 115 | stacklevel=2 116 | ) 117 | return self[self.keyOrder[index]] 118 | 119 | def insert(self, index, key, value): 120 | """Inserts the key, value pair before the item with the given index.""" 121 | warnings.warn( 122 | "SortedDict.insert is deprecated", PendingDeprecationWarning, 123 | stacklevel=2 124 | ) 125 | if key in self.keyOrder: 126 | n = self.keyOrder.index(key) 127 | del self.keyOrder[n] 128 | if n < index: 129 | index -= 1 130 | self.keyOrder.insert(index, key) 131 | super(SortedDict, self).__setitem__(key, value) 132 | 133 | def copy(self): 134 | """Returns a copy of this object.""" 135 | # This way of initializing the copy means it works for subclasses, too. 136 | return self.__class__(self) 137 | 138 | def __repr__(self): 139 | """ 140 | Replaces the normal dict.__repr__ with a version that returns the keys 141 | in their sorted order. 142 | """ 143 | return '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.iteritems()]) 144 | 145 | def clear(self): 146 | super(SortedDict, self).clear() 147 | self.keyOrder = [] 148 | 149 | 150 | class ConstType(type): 151 | def __new__(cls, name, bases, attrs): 152 | attrs_value = {} 153 | attrs_label = {} 154 | new_attrs = {} 155 | labels_to_values = {} 156 | 157 | for k, v in attrs.items(): 158 | if k.startswith('__'): 159 | continue 160 | if isinstance(v, tuple): 161 | attrs_value[k] = v[0] 162 | attrs_label[k] = v[1] 163 | new_attrs[v[0]] = v[1] 164 | labels_to_values[v[1]] = v[0] 165 | elif isinstance(v, dict) and 'label' in v: 166 | attrs_value[k] = v['value'] 167 | attrs_label[k] = v['label'] 168 | labels_to_values[v['label']] = v['value'] 169 | new_attrs[v['value']] = v['label'] 170 | else: 171 | attrs_value[k] = v 172 | attrs_label[k] = v 173 | 174 | sort_new_attrs = sorted(six.iteritems(new_attrs), key=lambda kv: k[0]) 175 | new_attrs = SortedDict(sort_new_attrs) 176 | 177 | obj = type.__new__(cls, name, bases, attrs_value) 178 | obj.values = attrs_value 179 | obj.labels = attrs_label 180 | obj.labels_to_values = labels_to_values 181 | obj.attrs = new_attrs 182 | return obj 183 | 184 | 185 | class Const(six.with_metaclass(ConstType)): 186 | pass 187 | -------------------------------------------------------------------------------- /ext_libs/weixin/client.py: -------------------------------------------------------------------------------- 1 | # -*-coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | """ 5 | File: client.py 6 | Author: goodspeed 7 | Email: cacique1103@gmail.com 8 | Github: https://github.com/zongxiao 9 | Date: 2015-02-06 10 | Description: Weixin OAuth2 11 | """ 12 | 13 | from . import oauth2 14 | from .bind import bind_method 15 | from .helper import genarate_signature 16 | 17 | 18 | SUPPORTED_FORMATS = ['', 'json'] 19 | 20 | 21 | class WeixinAPI(oauth2.OAuth2API): 22 | 23 | host = "open.weixin.qq.com" 24 | base_path = "" 25 | access_token_field = "access_token" 26 | authorize_url = "https://open.weixin.qq.com/connect/qrconnect" 27 | access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token" 28 | refresh_token_url = "https://api.weixin.qq.com/sns/oauth2/refresh_token" 29 | protocol = "https" 30 | api_name = "Weixin" 31 | x_ratelimit_remaining = None 32 | x_ratelimit = None 33 | 34 | def __init__(self, *args, **kwargs): 35 | format = kwargs.get('format', '') 36 | if format in SUPPORTED_FORMATS: 37 | self.format = format 38 | else: 39 | raise Exception("Unsupported format") 40 | super(WeixinAPI, self).__init__(*args, **kwargs) 41 | 42 | validate_token = bind_method(path='/sns/auth', 43 | accepts_parameters=['openid'], 44 | response_type="entry") 45 | 46 | user = bind_method(path="/sns/userinfo", 47 | accepts_parameters=["openid"], 48 | response_type="entry") 49 | 50 | 51 | class WeixinMpAPI(oauth2.OAuth2API): 52 | 53 | host = "open.weixin.qq.com" 54 | base_path = "" 55 | access_token_field = "access_token" 56 | authorize_url = "https://open.weixin.qq.com/connect/oauth2/authorize" 57 | access_token_url = "https://api.weixin.qq.com/sns/oauth2/access_token" 58 | refresh_token_url = "https://api.weixin.qq.com/sns/oauth2/refresh_token" 59 | client_credential_token_url = "https://api.weixin.qq.com/cgi-bin/token" 60 | protocol = "https" 61 | api_name = "WeixinMp" 62 | x_ratelimit_remaining = None 63 | x_ratelimit = None 64 | 65 | def __init__(self, *args, **kwargs): 66 | self.mp_token = kwargs.get('mp_token', None) 67 | self.timestamp = kwargs.get('timestamp', None) 68 | self.nonce = kwargs.get('nonce', None) 69 | self.signature = kwargs.get('signature', None) 70 | self.echostr = kwargs.get('echostr', None) 71 | self.xml_body = kwargs.get('xml_body', None) 72 | self.form_body = kwargs.get('form_body', None) 73 | self.json_body = kwargs.get('json_body', None) 74 | self.grant_type = kwargs.get('grant_type', None) 75 | format = kwargs.get('format', '') 76 | 77 | if format in SUPPORTED_FORMATS: 78 | self.format = format 79 | else: 80 | raise Exception("Unsupported format") 81 | super(WeixinMpAPI, self).__init__(*args, **kwargs) 82 | 83 | def validate_signature(self): 84 | params = { 85 | 'timestamp': self.timestamp, 86 | 'token': self.mp_token, 87 | 'nonce': self.nonce 88 | } 89 | signature = genarate_signature(params) 90 | return signature == self.signature 91 | 92 | user = bind_method(path="/sns/userinfo", 93 | accepts_parameters=["openid"], 94 | response_type="entry") 95 | 96 | validate_user = bind_method(path="/sns/auth", 97 | accepts_parameters=["openid"], 98 | response_type="entry") 99 | 100 | jsapi_ticket = bind_method(path='/cgi-bin/ticket/getticket', 101 | accepts_parameters=['type'], 102 | response_type="entry") 103 | 104 | create_menu = bind_method(path='/cgi-bin/menu/create', 105 | method='POST', 106 | accepts_parameters=['json_body'], 107 | response_type="entry") 108 | 109 | get_menu = bind_method(path='/cgi-bin/menu/get', 110 | accepts_parameters=['type'], 111 | response_type='entry') 112 | 113 | delete_menu = bind_method(path='/cgi-bin/menu/delete', 114 | accepts_parameters=['type'], 115 | response_type='entry') 116 | 117 | add_customservice = bind_method(path='/customservice/kfaccount/add', 118 | method='POST', 119 | accepts_parameters=['json_body'], 120 | response_type="entry") 121 | 122 | update_customservice = bind_method(path='/customservice/kfaccount/update', 123 | method='POST', 124 | accepts_parameters=['json_body'], 125 | response_type="entry") 126 | 127 | delete_customservice = bind_method(path='/customservice/kfaccount/delete', 128 | accepts_parameters=['json_body'], 129 | response_type="entry") 130 | 131 | getall_customservice = bind_method(path='/customservice/kfaccount/getkflist', 132 | accepts_parameters=['json_body'], 133 | response_type="entry") 134 | 135 | # TODO 待实现 136 | # uploadheadimg_customservice = bind_method( 137 | # path='/customservice/kfaccount/uploadheadimg', 138 | # method='POST', 139 | # accepts_parameters=['json_body'], 140 | # response_type="entry") 141 | 142 | custom_message_send = bind_method(path='/cgi-bin/message/custom/send', 143 | method='POST', 144 | accepts_parameters=['json_body'], 145 | response_type="entry") 146 | 147 | template_message_send = bind_method(path='/cgi-bin/message/template/send', 148 | method='POST', 149 | accepts_parameters=['json_body'], 150 | response_type="entry") 151 | 152 | qrcode = bind_method(path='/cgi-bin/qrcode/create', 153 | method='POST', 154 | accepts_parameters=['json_body'], 155 | response_type="entry") 156 | 157 | 158 | class WXAPPAPI(oauth2.OAuth2API): 159 | 160 | host = "api.weixin.qq.com" 161 | base_path = "" 162 | access_token_field = "access_token" 163 | authorize_url = "" 164 | access_token_url = "https://api.weixin.qq.com/sns/jscode2session" 165 | refresh_token_url = "" 166 | protocol = "https" 167 | api_name = "Weixin" 168 | x_ratelimit_remaining = None 169 | x_ratelimit = None 170 | 171 | def __init__(self, *args, **kwargs): 172 | format = kwargs.get('format', '') 173 | if format in SUPPORTED_FORMATS: 174 | self.format = format 175 | else: 176 | raise Exception("Unsupported format") 177 | super(WXAPPAPI, self).__init__(*args, **kwargs) 178 | -------------------------------------------------------------------------------- /ext_libs/weixin/bind.py: -------------------------------------------------------------------------------- 1 | # -*-coding: utf-8 -*- 2 | # !/usr/bin/env python 3 | from __future__ import unicode_literals 4 | 5 | """ 6 | File: bind.py 7 | Author: goodspeed 8 | Email: cacique1103@gmail.com 9 | Github: https://github.com/zongxiao 10 | Date: 2015-02-12 11 | Description: WeixinAPI bind 12 | """ 13 | 14 | import re 15 | import six 16 | import hmac 17 | from hashlib import sha256 18 | from six.moves.urllib.parse import quote 19 | 20 | from .oauth2 import OAuth2Request 21 | from .json_import import simplejson 22 | 23 | 24 | re_path_template = re.compile('{\w+}') 25 | 26 | 27 | def encode_string(value): 28 | return value.encode('utf-8') \ 29 | if isinstance(value, six.text_type) else str(value) 30 | 31 | 32 | class WeixinClientError(Exception): 33 | 34 | def __init__(self, error_message, status_code=None): 35 | self.status_code = status_code, 36 | self.error_message = error_message 37 | 38 | def __str__(self): 39 | if self.status_code: 40 | return "(%s) %s" % (self.status_code, self.error_message) 41 | else: 42 | return self.error_message 43 | 44 | 45 | class WeixinAPIError(Exception): 46 | 47 | def __init__(self, status_code, error_type, error_message, *args, **kwargs): 48 | self.status_code = status_code 49 | self.error_type = error_type 50 | self.error_message = error_message 51 | 52 | def __str__(self): 53 | return "(%s) %s-%s" % (self.status_code, self.error_type, 54 | self.error_message) 55 | 56 | 57 | def bind_method(**config): 58 | 59 | class WeixinAPIMethod(object): 60 | 61 | path = config['path'] 62 | method = config.get('method', 'GET') 63 | accepts_parameters = config.get("accepts_parameters", []) 64 | signature = config.get("signature", False) 65 | requires_target_user = config.get('requires_target_user', False) 66 | paginates = config.get('paginates', False) 67 | root_class = config.get('root_class', None) 68 | response_type = config.get("response_type", "list") 69 | include_secret = config.get("include_secret", False) 70 | objectify_response = config.get("objectify_response", True) 71 | 72 | def __init__(self, api, *args, **kwargs): 73 | self.api = api 74 | self.as_generator = kwargs.pop("as_generator", False) 75 | self.return_json = kwargs.pop("return_json", True) 76 | self.parameters = {} 77 | self._build_parameters(args, kwargs) 78 | self._build_path() 79 | 80 | def _build_parameters(self, args, kwargs): 81 | for index, value in enumerate(args): 82 | if value is None: 83 | continue 84 | try: 85 | self.parameters[self.accepts_parameters[index]] = encode_string(value) 86 | except IndexError: 87 | raise WeixinClientError("Too many arguments supplied") 88 | 89 | for key, value in six.iteritems(kwargs): 90 | if value is None: 91 | continue 92 | if key in self.parameters: 93 | raise WeixinClientError("Parameter %s already supplied" % key) 94 | if key not in set(['json_body']): 95 | value = encode_string(value) 96 | self.parameters[key] = value 97 | 98 | # if 'openid' in self.accepts_parameters and \ 99 | # 'openid' not in self.parameters and \ 100 | # not self.requires_target_user: 101 | # self.parameters['openid'] = 'self' 102 | 103 | def _build_path(self): 104 | for variable in re_path_template.findall(self.path): 105 | name = variable.strip('{}') 106 | 107 | try: 108 | value = quote(self.parameters[name]) 109 | except KeyError: 110 | raise Exception('No parameter value found for path variable: %s' % name) 111 | del self.parameters[name] 112 | 113 | self.path = self.path.replace(variable, value) 114 | 115 | if self.api.format: 116 | self.path = self.path + '.%s' % self.api.format 117 | else: 118 | self.path = self.path 119 | 120 | def _build_pagination_info(self, content_obj): 121 | pass 122 | 123 | def _do_api_request(self, url, method='GET', body=None, 124 | json_body=None, headers=None): 125 | headers = headers or {} 126 | if self.signature and self.api.app_secret is not None: 127 | secret = self.api.app_secret 128 | signature = hmac.new(secret, sha256).hexdigest() 129 | headers['X-Weixin-Forwarded-For'] = '|'.join([signature]) 130 | response = OAuth2Request(self.api).make_request( 131 | url, method=method, body=body, 132 | json_body=json_body, headers=headers) 133 | status_code = response.status_code 134 | try: 135 | content_obj = simplejson.loads(response.content) 136 | except ValueError: 137 | raise WeixinClientError( 138 | 'Unable to parse response, not valid JSON.', 139 | status_code=status_code) 140 | 141 | api_responses = [] 142 | if status_code == 200: 143 | if not self.objectify_response: 144 | return content_obj, None 145 | 146 | if self.response_type == 'list': 147 | for entry in content_obj['data']: 148 | if self.return_json: 149 | api_responses.append(entry) 150 | elif self.response_type == 'entry': 151 | data = content_obj 152 | if self.return_json: 153 | api_responses = data 154 | elif self.response_type == 'empty': 155 | pass 156 | return api_responses, self._build_pagination_info(content_obj) 157 | else: 158 | raise WeixinAPIError( 159 | status_code, content_obj['errcode'], content_obj['errmsg']) 160 | 161 | def _paginator_with_url(self, url, method="GET", body=None, headers=None): 162 | pass 163 | 164 | def _get_with_next_url(self, url, method="GET", body=None, headers=None): 165 | pass 166 | 167 | def execute(self): 168 | url, method, body, json_body, headers = ( 169 | OAuth2Request(self.api).prepare_request( 170 | self.method, self.path, self.parameters, 171 | include_secret=self.include_secret)) 172 | if self.as_generator: 173 | return self._paginator_with_url(url, method, body, headers) 174 | else: 175 | content, next = self._do_api_request(url, method, body, 176 | json_body, headers) 177 | if self.paginates: 178 | return content, next 179 | else: 180 | return content 181 | 182 | def _call(api, *args, **kwargs): 183 | method = WeixinAPIMethod(api, *args, **kwargs) 184 | return method.execute() 185 | 186 | return _call 187 | -------------------------------------------------------------------------------- /controllers/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | from datetime import date, datetime, time 5 | import pytz 6 | import functools 7 | 8 | from odoo import http, exceptions 9 | from odoo.http import request 10 | from odoo.loglevels import ustr 11 | 12 | from .. import defs 13 | 14 | import logging 15 | 16 | _logger = logging.getLogger(__name__) 17 | 18 | 19 | error_code = { 20 | -99: '', # 其他异常 21 | -2: u'用户名或密码不正确', 22 | -1: u'服务器内部错误', 23 | 0: u'接口调用成功', 24 | 403: u'禁止访问', 25 | 405: u'错误的请求类型', 26 | 501: u'数据库错误', 27 | 502: u'并发异常,请重试', 28 | 600: u'缺少参数', 29 | 601: u'无权操作:缺少 token', 30 | 602: u'签名错误', 31 | 609: u'token无效', 32 | 700: u'暂无数据', 33 | 701: u'该功能暂未开通', 34 | 702: u'资源余额不足', 35 | 901: u'登录超时', 36 | 902: u'登录超时',# 不触发授权登录 37 | 903: u'尚未登录', 38 | 300: u'缺少参数', 39 | 2000: u'当前登录token无效,请重新登录', 40 | 400: u'域名错误', 41 | 401: u'该域名已删除', 42 | 402: u'该域名已禁用', 43 | 404: u'暂无数据', 44 | 10000: u'微信用户未注册' 45 | } 46 | 47 | def jsonapi(f): 48 | @functools.wraps(f) 49 | def wrap(*args, **kw): 50 | try: 51 | return f(*args, **kw) 52 | except Exception as e: 53 | _logger.exception(str(e)) 54 | ret = {'code': -1, 'msg': str(e)} 55 | return request.make_response(json.dumps(ret, default=json_default)) 56 | return wrap 57 | 58 | 59 | class UserException(Exception): 60 | pass 61 | 62 | 63 | def json_default(obj): 64 | """ 65 | Properly serializes date and datetime objects. 66 | """ 67 | from odoo import fields 68 | if isinstance(obj, date): 69 | if isinstance(obj, datetime): 70 | return fields.Datetime.to_string(obj) 71 | return fields.Date.to_string(obj) 72 | return ustr(obj) 73 | 74 | class WechatUser(object): 75 | 76 | def __init__(self, partner, user, open_id=''): 77 | self.partner_id = partner 78 | self.user_id = user 79 | self.id = user.id 80 | self.open_id = open_id 81 | self.avatar_url = '' 82 | self.parent_id = False 83 | self.name = partner.name 84 | self.vat = '' 85 | self.category_id = False 86 | self.registered = False 87 | self.parent_key = '' 88 | self.birthday = '' 89 | 90 | def check_account_ok(self): 91 | return True 92 | 93 | @property 94 | def address_ids(self): 95 | return self.partner_id.child_ids.filtered(lambda r: r.type == 'delivery') 96 | 97 | def get_product_pricelist(self): 98 | return self.partner_id.property_product_pricelist 99 | 100 | class BaseController(object): 101 | 102 | def _check_domain(self, sub_domain): 103 | wxapp_entry = request.env['wxapp.config'].sudo().get_entry(sub_domain) 104 | if not wxapp_entry: 105 | return self.res_err(404), None 106 | self._makeup_context(request.env, wxapp_entry) 107 | if wxapp_entry.need_login(): 108 | if not request.session.get('login_uid'): 109 | return self.res_err(903), None 110 | return None, wxapp_entry 111 | 112 | def _makeup_context(self, env, entry): 113 | fm_type=request.httprequest.cookies.get('_fm') 114 | header_fm_type = request.httprequest.headers.get("Fm-Type") 115 | if header_fm_type: 116 | fm_type = header_fm_type 117 | env.context = dict(env.context, entry_id=entry.get_id(), fm_type=fm_type, entry_company_id=entry.get_company_id()) 118 | entry.env.context = dict(entry.env.context, entry_id=entry.get_id(), fm_type=fm_type, entry_company_id=entry.get_company_id()) 119 | 120 | def _check_user(self, sub_domain, token): 121 | wxapp_entry = request.env['wxapp.config'].sudo().get_entry(sub_domain) 122 | if not wxapp_entry: 123 | return self.res_err(404), None, wxapp_entry 124 | self._makeup_context(request.env, wxapp_entry) 125 | if not token: 126 | return self.res_err(2000), None, wxapp_entry 127 | 128 | login_uid = request.session.get('login_uid') 129 | _logger.info('>>> get session login_uid %s', login_uid) 130 | if login_uid: 131 | if str(login_uid)==token:# request.session.sid==token: 132 | wechat_user = request.env['wxapp.user'].sudo().search([('partner_id', '=', request.env.user.partner_id.id)], limit=1) 133 | if wechat_user: 134 | request.wechat_user = wechat_user 135 | return None, wechat_user, wxapp_entry 136 | #else: 137 | # wechat_user = WechatUser(request.env.user.partner_id, request.env.user) 138 | # request.wechat_user = wechat_user 139 | # return None, wechat_user, wxapp_entry 140 | 141 | access_token = request.env['wxapp.access_token'].sudo().search([ 142 | ('token', '=', token), 143 | #('create_uid', '=', user.id) 144 | ]) 145 | 146 | if not access_token: 147 | return self.res_err(901), None, wxapp_entry 148 | 149 | wechat_user = request.env['wxapp.user'].sudo().search([ 150 | ('open_id', '=', access_token.open_id), 151 | #('create_uid', '=', user.id) 152 | ]) 153 | 154 | if not wechat_user: 155 | return self.res_err(10000), None, wxapp_entry 156 | 157 | request.wechat_user = wechat_user 158 | return None, wechat_user, wxapp_entry 159 | 160 | def check_userid(self, token): 161 | if token: 162 | login_uid = request.session.get('login_uid') 163 | if login_uid: 164 | if str(login_uid)==token: 165 | wechat_user = request.env['wxapp.user'].sudo().search([('partner_id', '=', request.env.user.partner_id.id)], limit=1) 166 | if wechat_user: 167 | request.wechat_user = wechat_user 168 | else: 169 | wechat_user = WechatUser(request.env.user.partner_id, request.env.user) 170 | request.wechat_user = wechat_user 171 | 172 | access_token = request.env['wxapp.access_token'].sudo().search([ 173 | ('token', '=', token), 174 | ]) 175 | if not access_token: 176 | return 177 | wechat_user = request.env['wxapp.user'].sudo().search([ 178 | ('open_id', '=', access_token.open_id), 179 | ]) 180 | if wechat_user: 181 | request.wechat_user = wechat_user 182 | 183 | 184 | def res_ok(self, data=None): 185 | ret = {'code': 0, 'msg': 'success'} 186 | if data!=None: 187 | ret['data'] = data 188 | return request.make_response( 189 | headers={'Content-Type': 'json'}, 190 | data=json.dumps(ret, default=json_default) 191 | ) 192 | 193 | def res_err(self, code, data=None): 194 | ret = {'code': code, 'msg': error_code.get(code) or data} 195 | if data: 196 | ret['data'] = data 197 | return request.make_response(json.dumps(ret, default=json_default)) 198 | 199 | 200 | def convert_static_link(request, html): 201 | base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url') 202 | return html.replace('src="/', 'src="{base_url}/'.format(base_url=base_url)) 203 | 204 | 205 | def dt_convert(value, return_format='%Y-%m-%d %H:%M:%S', gmt_diff=8): 206 | """ 207 | UTC时间转为本地时间 208 | """ 209 | if not value: 210 | return value 211 | if isinstance(value, datetime): 212 | value = value.strftime(return_format) 213 | dt = datetime.strptime(value, return_format) 214 | _diff = '' 215 | if gmt_diff>0: 216 | _diff = '-%s'%gmt_diff 217 | else: 218 | _diff = '+%s'%(0-gmt_diff) 219 | pytz_timezone = pytz.timezone('Etc/GMT' + _diff) 220 | dt = dt.replace(tzinfo=pytz.timezone('UTC')) 221 | return dt.astimezone(pytz_timezone).strftime(return_format) 222 | 223 | def dt_utc(value, return_format='%Y-%m-%d %H:%M:%S', gmt_diff=8): 224 | """ 225 | 本地时间转为UTC时间 226 | """ 227 | if not value: 228 | return value 229 | if isinstance(value, datetime): 230 | value = value.strftime(return_format) 231 | dt = datetime.strptime(value, return_format) 232 | _diff = '' 233 | if gmt_diff>=0: 234 | _diff = '+%s'%gmt_diff 235 | else: 236 | _diff = str(gmt_diff) 237 | pytz_timezone = pytz.timezone('Etc/GMT' + _diff) 238 | dt = dt.replace(tzinfo=pytz.timezone('UTC')) 239 | return dt.astimezone(pytz_timezone).strftime(return_format) 240 | -------------------------------------------------------------------------------- /ext_libs/weixin/lib/WXBizMsgCrypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 对公众平台发送给公众账号的消息加解密示例代码. 4 | @copyright: Copyright (c) 1998-2014 Tencent Inc. 5 | 6 | """ 7 | # TODO 重构加密解密 8 | 9 | from imp import reload 10 | 11 | import base64 12 | import random 13 | import hashlib 14 | import time 15 | import struct 16 | from Crypto.Cipher import AES 17 | import xml.etree.cElementTree as ET 18 | import sys 19 | import socket 20 | 21 | reload(sys) 22 | 23 | from .ierror import * 24 | 25 | from ..helper import smart_bytes, safe_char 26 | 27 | 28 | class FormatException(Exception): 29 | pass 30 | 31 | 32 | def throw_exception(message, exception_class=FormatException): 33 | """my define raise exception function""" 34 | raise exception_class(message) 35 | 36 | 37 | class SHA1: 38 | """计算公众平台的消息签名接口""" 39 | 40 | def getSHA1(self, token, timestamp, nonce, encrypt): 41 | """用SHA1算法生成安全签名 42 | @param token: 票据 43 | @param timestamp: 时间戳 44 | @param encrypt: 密文 45 | @param nonce: 随机字符串 46 | @return: 安全签名 47 | """ 48 | try: 49 | sortlist = [token, timestamp, nonce, encrypt] 50 | sortlist.sort() 51 | sha = hashlib.sha1() 52 | sha.update(str("".join(sortlist)).encode('utf-8')) 53 | return WXBizMsgCrypt_OK, sha.hexdigest() 54 | except Exception: 55 | return WXBizMsgCrypt_ComputeSignature_Error, None 56 | 57 | 58 | class XMLParse: 59 | """提供提取消息格式中的密文及生成回复消息格式的接口""" 60 | 61 | # xml消息模板 62 | AES_TEXT_RESPONSE_TEMPLATE = """ 63 | 64 | 65 | 66 | %(timestamp)s 67 | 68 | 69 | """ 70 | 71 | def extract(self, xmltext): 72 | """提取出xml数据包中的加密消息 73 | @param xmltext: 待提取的xml字符串 74 | @return: 提取出的加密消息字符串 75 | """ 76 | try: 77 | xml_tree = ET.fromstring(xmltext) 78 | encrypt = xml_tree.find("Encrypt") 79 | touser_name = xml_tree.find("ToUserName") 80 | return WXBizMsgCrypt_OK, encrypt.text, touser_name.text 81 | except Exception: 82 | return WXBizMsgCrypt_ParseXml_Error, None, None 83 | 84 | def generate(self, encrypt, signature, timestamp, nonce): 85 | """生成xml消息 86 | @param encrypt: 加密后的消息密文 87 | @param signature: 安全签名 88 | @param timestamp: 时间戳 89 | @param nonce: 随机字符串 90 | @return: 生成的xml字符串 91 | """ 92 | resp_dict = { 93 | 'msg_encrypt': encrypt, 94 | 'msg_signaturet': signature, 95 | 'timestamp': timestamp, 96 | 'nonce': nonce 97 | } 98 | resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict 99 | return resp_xml 100 | 101 | 102 | class PKCS7Encoder(): 103 | """提供基于PKCS7算法的加解密接口""" 104 | 105 | block_size = 32 106 | 107 | def encode(self, text): 108 | """ 对需要加密的明文进行填充补位 109 | @param text: 需要进行填充补位操作的明文 110 | @return: 补齐明文字符串 111 | """ 112 | text_length = len(text) 113 | # 计算需要填充的位数 114 | amount_to_pad = self.block_size - (text_length % self.block_size) 115 | if amount_to_pad == 0: 116 | amount_to_pad = self.block_size 117 | # 获得补位所用的字符 118 | pad = smart_bytes(chr(amount_to_pad)) 119 | return text + pad * amount_to_pad 120 | 121 | def decode(self, decrypted): 122 | """删除解密后明文的补位字符 123 | @param decrypted: 解密后的明文 124 | @return: 删除补位字符后的明文 125 | """ 126 | pad = ord(decrypted[-1]) 127 | if pad < 1 or pad > 32: 128 | pad = 0 129 | return decrypted[:-pad] 130 | 131 | 132 | class Prpcrypt(object): 133 | """提供接收和推送给公众平台消息的加解密接口""" 134 | 135 | def __init__(self, key): 136 | # self.key = base64.b64decode(key+"=") 137 | self.key = key 138 | # 设置加解密模式为AES的CBC模式 139 | self.mode = AES.MODE_CBC 140 | 141 | def encrypt(self, text, appid): 142 | """对明文进行加密 143 | @param text: 需要加密的明文 144 | @return: 加密得到的字符串 145 | """ 146 | # 16位随机字符串添加到明文开头 147 | pack_str = struct.pack(b"I", socket.htonl(len(text))) 148 | text = smart_bytes(self.get_random_str()) + pack_str + smart_bytes(text) + smart_bytes(appid) 149 | # 使用自定义的填充方式对明文进行补位填充 150 | pkcs7 = PKCS7Encoder() 151 | text = pkcs7.encode(text) 152 | # 加密 153 | cryptor = AES.new(self.key, self.mode, self.key[:16]) 154 | try: 155 | ciphertext = cryptor.encrypt(text) 156 | # 使用BASE64对加密后的字符串进行编码 157 | return WXBizMsgCrypt_OK, base64.b64encode(ciphertext) 158 | except Exception: 159 | return WXBizMsgCrypt_EncryptAES_Error, None 160 | 161 | def decrypt(self, text, appid): 162 | """对解密后的明文进行补位删除 163 | @param text: 密文 164 | @return: 删除填充补位后的明文 165 | """ 166 | try: 167 | cryptor = AES.new(self.key, self.mode, self.key[:16]) 168 | # 使用BASE64对密文进行解码,然后AES-CBC解密 169 | plain_text = cryptor.decrypt(base64.b64decode(text)) 170 | except Exception: 171 | return WXBizMsgCrypt_DecryptAES_Error, None 172 | try: 173 | pad = plain_text[-1] 174 | # 去掉补位字符串 175 | # pkcs7 = PKCS7Encoder() 176 | # plain_text = pkcs7.encode(plain_text) 177 | # 去除16位随机字符串 178 | content = plain_text[16:-pad] 179 | xml_len = socket.ntohl(struct.unpack(b"I", content[:4])[0]) 180 | xml_content = content[4:xml_len+4] 181 | from_appid = smart_bytes(content[xml_len+4:]) 182 | except Exception: 183 | return WXBizMsgCrypt_IllegalBuffer, None 184 | if from_appid != smart_bytes(appid): 185 | return WXBizMsgCrypt_ValidateAppid_Error, None 186 | return 0, xml_content 187 | 188 | def get_random_str(self): 189 | """ 随机生成16位字符串 190 | @return: 16位字符串 191 | """ 192 | str_list = random.sample(safe_char[:-4], 16) 193 | sl = [chr(s) for s in str_list] 194 | return "".join(sl) 195 | 196 | 197 | class WXBizMsgCrypt(object): 198 | 199 | # 构造函数 200 | # @param sToken: 公众平台上,开发者设置的Token 201 | # @param sEncodingAESKey: 公众平台上,开发者设置的EncodingAESKey 202 | # @param sAppId: 企业号的AppId 203 | def __init__(self, sToken, sEncodingAESKey, sAppId): 204 | try: 205 | self.key = base64.b64decode(sEncodingAESKey + "=") 206 | assert len(self.key) == 32 207 | except: 208 | throw_exception("[error]: EncodingAESKey unvalid !", 209 | FormatException) 210 | self.token = sToken 211 | self.appid = sAppId 212 | 213 | def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): 214 | # 将公众号回复用户的消息加密打包 215 | # @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 216 | # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 217 | # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce 218 | # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, 219 | # return:成功0,sEncryptMsg,失败返回对应的错误码None 220 | pc = Prpcrypt(self.key) 221 | ret, encrypt = pc.encrypt(sReplyMsg, self.appid) 222 | if ret != 0: 223 | return ret, None 224 | if timestamp is None: 225 | timestamp = str(int(time.time())) 226 | # 生成安全签名 227 | sha1 = SHA1() 228 | ret, signature = sha1.getSHA1(self.token, timestamp, sNonce, encrypt) 229 | if ret != 0: 230 | return ret, None 231 | xmlParse = XMLParse() 232 | return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce) 233 | 234 | def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): 235 | # 检验消息的真实性,并且获取解密后的明文 236 | # @param sMsgSignature: 签名串,对应URL参数的msg_signature 237 | # @param sTimeStamp: 时间戳,对应URL参数的timestamp 238 | # @param sNonce: 随机串,对应URL参数的nonce 239 | # @param sPostData: 密文,对应POST请求的数据 240 | # xml_content: 解密后的原文,当return返回0时有效 241 | # @return: 成功0,失败返回对应的错误码 242 | # 验证安全签名 243 | xmlParse = XMLParse() 244 | ret, encrypt, touser_name = xmlParse.extract(sPostData) 245 | if ret != 0: 246 | return ret, None 247 | sha1 = SHA1() 248 | ret, signature = sha1.getSHA1(self.token, sTimeStamp, sNonce, encrypt) 249 | if ret != 0: 250 | return ret, None 251 | if not signature == sMsgSignature: 252 | return WXBizMsgCrypt_ValidateSignature_Error, None 253 | pc = Prpcrypt(self.key) 254 | ret, xml_content = pc.decrypt(encrypt, self.appid) 255 | return ret, xml_content 256 | -------------------------------------------------------------------------------- /controllers/address.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | 8 | from .. import defs 9 | from .base import BaseController 10 | 11 | import logging 12 | 13 | _logger = logging.getLogger(__name__) 14 | 15 | 16 | class WxappAddress(http.Controller, BaseController): 17 | 18 | def _get_address_dict(self, each_address, wxapp_user_id): 19 | _dict = { 20 | "address": each_address.street, 21 | "areaStr": each_address.district_id.name or '', 22 | "cityId": each_address.city_id.id, 23 | "cityStr": each_address.city_id.name or '', 24 | "code": each_address.zip, 25 | "dateAdd": each_address.create_date, 26 | "dateUpdate": each_address.write_date, 27 | "districtId": each_address.district_id.id or False, 28 | "id": each_address.id, 29 | "isDefault": each_address.is_default, 30 | "linkMan": each_address.name or '', 31 | "mobile": each_address.mobile or '', 32 | "provinceId": each_address.province_id.id, 33 | "provinceStr": each_address.province_id.name or '', 34 | "status": 0 if each_address.active else 1, 35 | "statusStr": '正常' if each_address.active else '禁用', 36 | "uid": each_address.create_uid.id, 37 | "userId": wxapp_user_id 38 | } 39 | return _dict 40 | 41 | @http.route(['/wxa//user/shipping-address/list', '/wxa//user/shipping-address/list/v2'], auth='public', methods=['GET', 'POST'], csrf=False) 42 | def list(self, sub_domain, token=None): 43 | try: 44 | res, wechat_user, entry = self._check_user(sub_domain, token) 45 | if res:return res 46 | 47 | if not wechat_user.address_ids: 48 | return self.res_err(700) 49 | 50 | data = [self._get_address_dict(each_address, wechat_user.id) for each_address in wechat_user.address_ids.filtered(lambda r: r.active)] 51 | if '/v2' in request.httprequest.url: 52 | return self.res_ok({ 53 | 'result': data, 54 | 'totalPage': 1, 55 | 'totalRow': len(data), 56 | }) 57 | return self.res_ok(data) 58 | 59 | except Exception as e: 60 | _logger.exception(e) 61 | return self.res_err(-1, str(e)) 62 | 63 | 64 | @http.route('/wxa//user/shipping-address/add', auth='public', methods=['GET','POST'], csrf=False, type='http') 65 | def add(self, sub_domain, **kwargs): 66 | try: 67 | token = kwargs.get('token', None) 68 | 69 | res, wechat_user, entry = self._check_user(sub_domain, token) 70 | if res:return res 71 | 72 | new_address = request.env(user=1)['res.partner'].create({ 73 | 'parent_id': wechat_user.partner_id.id, 74 | 'name': kwargs['linkMan'], 75 | 'mobile': kwargs['mobile'], 76 | 'province_id': int(kwargs['provinceId']) if kwargs.get('provinceId') else False, 77 | 'city_id': int(kwargs['cityId']) if kwargs.get('cityId') else False, 78 | 'district_id': int(kwargs['districtId']) if kwargs.get('districtId') else False, 79 | 'street': kwargs['address'], 80 | 'zip': kwargs.get('code', False), 81 | 'type': 'delivery', 82 | 'is_default': json.loads(kwargs['isDefault']) 83 | }) 84 | address = new_address 85 | _main = '%s %s %s'%(address.province_id.name, address.city_id.name, address.district_id.name or '') 86 | address.write({'street2': _main}) 87 | 88 | address_ids = wechat_user.address_ids.filtered(lambda r: r.id != new_address.id) 89 | if address_ids: 90 | address_ids.write({'is_default': False}) 91 | 92 | return self.res_ok() 93 | 94 | except Exception as e: 95 | _logger.exception(e) 96 | return self.res_err(-1, str(e)) 97 | 98 | 99 | @http.route('/wxa//user/shipping-address/update', auth='public', methods=['GET','POST'], csrf=False, type='http') 100 | def update(self, sub_domain, **kwargs): 101 | try: 102 | token = kwargs.get('token', None) 103 | 104 | res, wechat_user, entry = self._check_user(sub_domain, token) 105 | if res:return res 106 | 107 | address = request.env(user=1)['res.partner'].browse(int(kwargs['id'])) 108 | 109 | if not address: 110 | return self.res_err(404) 111 | 112 | address.write({ 113 | 'name': kwargs['linkMan'] if kwargs.get('linkMan') else address.name, 114 | 'mobile': kwargs['mobile'] if kwargs.get('mobile') else address.mobile, 115 | 'province_id': int(kwargs['provinceId']) if kwargs.get('provinceId') else address.province_id.id, 116 | 'city_id': int(kwargs['cityId']) if kwargs.get('cityId') else address.city_id.id, 117 | 'district_id': int(kwargs['districtId']) if kwargs.get('districtId') else address.district_id.id, 118 | 'street': kwargs['address'] if kwargs.get('address') else address.street, 119 | 'zip': kwargs['code'] if kwargs.get('code') else address.zip, 120 | 'is_default': json.loads(kwargs['isDefault']) 121 | }) 122 | _main = '%s %s %s'%(address.province_id.name, address.city_id.name, address.district_id.name or '') 123 | address.write({'street2': _main}) 124 | 125 | address_ids = wechat_user.address_ids.filtered(lambda r: r.id != address.id) 126 | if address_ids and address.is_default: 127 | address_ids.write({'is_default': False}) 128 | 129 | return self.res_ok() 130 | 131 | except Exception as e: 132 | _logger.exception(e) 133 | return self.res_err(-1, str(e)) 134 | 135 | 136 | @http.route('/wxa//user/shipping-address/delete', auth='public', methods=['GET', 'POST'], csrf=False) 137 | def delete(self, sub_domain, token=None, id=None, **kwargs): 138 | address_id = id 139 | try: 140 | res, wechat_user, entry = self._check_user(sub_domain, token) 141 | if res:return res 142 | 143 | if not address_id: 144 | return self.res_err(300) 145 | 146 | address = request.env(user=1)['res.partner'].browse(int(address_id)) 147 | 148 | if not address: 149 | return self.res_err(404) 150 | 151 | address.write({'active': False}) 152 | 153 | if wechat_user.address_ids: 154 | wechat_user.address_ids[0].write({'is_default': True}) 155 | 156 | return self.res_ok() 157 | 158 | except Exception as e: 159 | _logger.exception(e) 160 | return self.res_err(-1, str(e)) 161 | 162 | def get_default_ext(self, data, wechat_user): 163 | return data 164 | 165 | @http.route(['/wxa//user/shipping-address/default', '/wxa//user/shipping-address/default/v2'], auth='public', methods=['GET']) 166 | def default(self, sub_domain, token=None, **kwargs): 167 | try: 168 | res, wechat_user, entry = self._check_user(sub_domain, token) 169 | if res:return res 170 | 171 | address = request.env(user=1)['res.partner'].search([ 172 | ('parent_id', '=', wechat_user.partner_id.id), 173 | ('type', '=', 'delivery'), 174 | ('is_default', '=', True) 175 | ], limit=1) 176 | 177 | if not address: 178 | return self.res_err(404, self.get_default_ext({}, wechat_user)) 179 | 180 | data = self._get_address_dict(address, wechat_user.id) 181 | if '/v2' in request.httprequest.url: 182 | return self.res_ok({'extJson': {}, 'info': self.get_default_ext(data, wechat_user)}) 183 | return self.res_ok(self.get_default_ext(data, wechat_user)) 184 | 185 | except Exception as e: 186 | _logger.exception(e) 187 | return self.res_err(-1, str(e)) 188 | 189 | 190 | @http.route(['/wxa//user/shipping-address/detail', '/wxa//user/shipping-address/detail/v2'], auth='public', methods=['GET']) 191 | def detail(self, sub_domain, token=None, id=None, **kwargs): 192 | address_id = id 193 | try: 194 | res, wechat_user, entry = self._check_user(sub_domain, token) 195 | if res:return res 196 | 197 | if not address_id: 198 | return self.res_err(300) 199 | 200 | address = request.env(user=1)['res.partner'].browse(int(address_id)) 201 | 202 | if not address: 203 | return self.res_err(404) 204 | 205 | if '/v2' in request.httprequest.url: 206 | return self.res_ok({'info': self._get_address_dict(address, wechat_user.id)}) 207 | return self.res_ok(self._get_address_dict(address, wechat_user.id)) 208 | 209 | except Exception as e: 210 | _logger.exception(e) 211 | return self.res_err(-1, str(e)) 212 | -------------------------------------------------------------------------------- /ext_libs/weixin/reply.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | # https://github.com/wechat-python-sdk/wechat-python-sdk/blob/master/wechat_sdk/reply.py 5 | 6 | import time 7 | 8 | from .msg_template import (TEXT_TEMPLATE, IMAGE_TEMPLATE, 9 | VOICE_TEMPLATE, VIDEO_TEMPLATE, 10 | THUM_MUSIC_TEMPLATE, NOTHUM_MUSIC_TEMPLATE, 11 | ARITICLE_TEMPLATE, ARITICLE_ITEM_TEMPLATE) 12 | 13 | 14 | class Article(object): 15 | def __init__(self, title=None, description=None, picurl=None, url=None): 16 | self.title = title or '' 17 | self.description = description or '' 18 | self.picurl = picurl or '' 19 | self.url = url or '' 20 | 21 | 22 | class WXReply(object): 23 | 24 | def __init__(self, to_user=None, from_user=None, 25 | create_time=None, **kwargs): 26 | ''' 27 | MsgType: text|image|voice|video|music|news 28 | ''' 29 | kwargs['to_user'] = to_user 30 | kwargs['from_user'] = from_user 31 | kwargs['create_time'] = create_time or int(time.time()) 32 | 33 | self.params = {k: v for k, v in kwargs.items() if kwargs[k]} 34 | 35 | def render(self): 36 | raise NotImplementedError() 37 | 38 | 39 | class TextReply(WXReply): 40 | """ 41 | 回复文字消息 42 | """ 43 | TEMPLATE = TEXT_TEMPLATE 44 | 45 | def __init__(self, content, *args, **kwargs): 46 | """ 47 | :param content: 文字回复内容 48 | """ 49 | super(TextReply, self).__init__(content=content, *args, **kwargs) 50 | 51 | def render(self): 52 | return self.TEMPLATE.format(**self.params) 53 | 54 | 55 | class ImageReply(WXReply): 56 | """ 57 | 回复图片消息 58 | """ 59 | TEMPLATE = IMAGE_TEMPLATE 60 | 61 | def __init__(self, media_id): 62 | """ 63 | :param media_id: 图片的 MediaID 64 | """ 65 | super(ImageReply, self).__init__(media_id=media_id) 66 | 67 | def render(self): 68 | return self.TEMPLATE.format(**self.params) 69 | 70 | 71 | class VoiceReply(WXReply): 72 | """ 73 | 回复语音消息 74 | """ 75 | TEMPLATE = VOICE_TEMPLATE 76 | 77 | def __init__(self, media_id): 78 | """ 79 | :param media_id: 语音的 MediaID 80 | """ 81 | super(VoiceReply, self).__init__(media_id=media_id) 82 | 83 | def render(self): 84 | return self.TEMPLATE.format(**self.params) 85 | 86 | 87 | class VideoReply(WXReply): 88 | """ 89 | 回复视频消息 90 | """ 91 | 92 | TEMPLATE = VIDEO_TEMPLATE 93 | 94 | def __init__(self, media_id, title=None, description=None): 95 | """ 96 | :param media_id: 视频的 MediaID 97 | :param title: 视频消息的标题 98 | :param description: 视频消息的描述 99 | """ 100 | title = title or '' 101 | description = description or '' 102 | super(VideoReply, self).__init__(media_id=media_id, title=title, 103 | description=description) 104 | 105 | def render(self): 106 | return self.TEMPLATE.format(**self.params) 107 | 108 | 109 | class MusicReply(WXReply): 110 | """ 111 | 回复音乐消息 112 | """ 113 | TEMPLATE_THUMB = THUM_MUSIC_TEMPLATE 114 | TEMPLATE_NOTHUMB = NOTHUM_MUSIC_TEMPLATE 115 | 116 | def __init__(self, title='', description='', music_url='', 117 | hq_music_url='', thumb_media_id=None): 118 | title = title or '' 119 | description = description or '' 120 | music_url = music_url or '' 121 | hq_music_url = hq_music_url or music_url 122 | super(MusicReply, self).__init__( 123 | title=title, description=description, music_url=music_url, 124 | hq_music_url=hq_music_url, thumb_media_id=thumb_media_id) 125 | 126 | def render(self): 127 | if self._args['thumb_media_id']: 128 | return self.TEMPLATE.format(**self.params) 129 | else: 130 | return self.TEMPLATE.format(**self.params) 131 | 132 | 133 | class ArticleReply(WXReply): 134 | 135 | TEMPLATE = ARITICLE_TEMPLATE 136 | ITEM_TEMPLATE = ARITICLE_ITEM_TEMPLATE 137 | 138 | def __init__(self, **kwargs): 139 | super(ArticleReply, self).__init__(**kwargs) 140 | self._articles = [] 141 | 142 | def add_article(self, article): 143 | if len(self._articles) >= 8: 144 | raise AttributeError( 145 | "Can't add more than 8 articles in an ArticleReply") 146 | else: 147 | self._articles.append(article) 148 | 149 | def render(self): 150 | items = [] 151 | for article in self._articles: 152 | items.append(ArticleReply.ITEM_TEMPLATE.format( 153 | title=article.title, 154 | description=article.description, 155 | picurl=article.picurl, 156 | url=article.url, 157 | )) 158 | self.params["items"] = ''.join(items) 159 | self.params["count"] = len(items) 160 | return self.TEMPLATE.format(**self.params) 161 | 162 | 163 | class WXCustomReply(object): 164 | 165 | def __init__(self, to_user=None, msgtype=None, **kwargs): 166 | ''' 167 | MsgType: text|image|voice|video|music|news 168 | ''' 169 | kwargs['to_user'] = to_user 170 | kwargs['msgtype'] = msgtype 171 | 172 | self.params = {k: v for k, v in kwargs.items() if kwargs[k]} 173 | 174 | def render(self): 175 | raise NotImplementedError() 176 | 177 | 178 | class CustomTextReply(WXCustomReply): 179 | """ 180 | 回复文字消息 181 | """ 182 | 183 | def __init__(self, content, *args, **kwargs): 184 | """ 185 | :param content: 文字回复内容 186 | """ 187 | super(CustomTextReply, self).__init__(msgtype='text', 188 | *args, **kwargs) 189 | self.content = content 190 | 191 | def render(self): 192 | self.params['text'] = { 193 | 'content': self.content 194 | } 195 | return self.params 196 | 197 | 198 | class CustomImageReply(WXCustomReply): 199 | """ 200 | 回复图片消息 201 | """ 202 | 203 | def __init__(self, media_id): 204 | """ 205 | :param media_id: 图片的 MediaID 206 | """ 207 | super(CustomImageReply, self).__init__(msgtype='image') 208 | self.media_id = media_id 209 | 210 | def render(self): 211 | self.params['image'] = { 212 | 'media_id': self.media_id 213 | } 214 | return self.params 215 | 216 | 217 | class CustomVoiceReply(WXCustomReply): 218 | """ 219 | 回复语音消息 220 | """ 221 | 222 | def __init__(self, media_id): 223 | """ 224 | :param media_id: 语音的 MediaID 225 | """ 226 | super(CustomVoiceReply, self).__init__(msgtype='voice') 227 | self.media_id = media_id 228 | 229 | def render(self): 230 | self.params['image'] = { 231 | 'media_id': self.media_id 232 | } 233 | return self.params 234 | 235 | 236 | class CustomVideoReply(WXCustomReply): 237 | """ 238 | 回复视频消息 239 | """ 240 | 241 | def __init__(self, media_id, title=None, description=None): 242 | """ 243 | :param media_id: 视频的 MediaID 244 | :param title: 视频消息的标题 245 | :param description: 视频消息的描述 246 | """ 247 | super(CustomVideoReply, self).__init__(msgtype='music') 248 | self.media_id = media_id 249 | self.title = title or '' 250 | self.description = description or '' 251 | 252 | def render(self): 253 | self.params['video'] = { 254 | 'media_id': self.media_id, 255 | 'title': self.title, 256 | 'description': self.description 257 | } 258 | return self.params 259 | 260 | 261 | class CustomMusicReply(WXCustomReply): 262 | """ 263 | 回复音乐消息 264 | """ 265 | 266 | def __init__(self, title='', description='', music_url='', 267 | hq_music_url='', thumb_media_id=None): 268 | self.title = title or '' 269 | self.description = description or '' 270 | self.musicurl = music_url or '' 271 | self.hqmusicurl = hq_music_url or music_url 272 | self.thumb_media_id = thumb_media_id 273 | super(CustomMusicReply, self).__init__(msgtype='music') 274 | 275 | def render(self): 276 | self.params['music'] = { 277 | 'title': self.title, 278 | 'description': self.description, 279 | 'musicurl': self.musicurl, 280 | 'hqmusicurl': self.hqmusicurl, 281 | 'thumb_media_id': self.thumb_media_id, 282 | } 283 | return self.params 284 | 285 | 286 | class CustomArticleReply(WXCustomReply): 287 | 288 | def __init__(self, **kwargs): 289 | self._articles = [] 290 | super(CustomArticleReply, self).__init__(msgtype='news', **kwargs) 291 | 292 | def add_article(self, article): 293 | ''' 294 | :params article (dict) 295 | { 296 | "title":"Happy Day", 297 | "description":"Is Really A Happy Day", 298 | "url":"URL", 299 | "picurl":"PIC_URL" 300 | } 301 | 302 | ''' 303 | if len(self._articles) >= 8: 304 | raise AttributeError( 305 | "Can't add more than 8 articles in an CustomArticleReply") 306 | else: 307 | self._articles.append(article) 308 | 309 | def render(self): 310 | items = [] 311 | for article in self._articles: 312 | items.append(article) 313 | self.params['news'] = {'articles': items} 314 | return self.params 315 | -------------------------------------------------------------------------------- /controllers/product.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import json 4 | 5 | from odoo import http 6 | from odoo.http import request 7 | 8 | from .. import defs 9 | from .base import BaseController 10 | from .base import convert_static_link 11 | 12 | import logging 13 | 14 | _logger = logging.getLogger(__name__) 15 | 16 | 17 | class WxappProduct(http.Controller, BaseController): 18 | 19 | 20 | def _product_basic_dict(self, each_goods): 21 | _dict = { 22 | "categoryId": each_goods.wxpp_category_id.id, 23 | "characteristic": each_goods.characteristic, 24 | "dateAdd": each_goods.create_date, 25 | "dateUpdate": each_goods.write_date, 26 | "id": each_goods.id, 27 | "index": each_goods.id, 28 | "logisticsId": 1, 29 | "minPrice": request.env['product.template'].cli_price(each_goods.get_present_price(1)), 30 | #"uom_name": 'uom_name' in each_goods._fields.keys() and each_goods.uom_name or each_goods.uom_id.name, 31 | "minScore": 0, 32 | "minBuyNumber": 1, 33 | "name": each_goods.name, 34 | "numberFav": each_goods.number_fav, 35 | "numberGoodReputation": 0, 36 | "numberOrders": 0,#each_goods.sales_count, 37 | "originalPrice": each_goods.original_price, 38 | "paixu": each_goods.sequence or 0, 39 | "pic": each_goods.main_img, 40 | "recommendStatus": 0 if not each_goods.recommend_status else 1, 41 | "recommendStatusStr": defs.GoodsRecommendStatus.attrs[each_goods.recommend_status], 42 | "shopId": 0, 43 | "status": 0 if each_goods.wxapp_published else 1, 44 | "statusStr": '上架' if each_goods.wxapp_published else '下架', 45 | "stores": each_goods.get_present_qty(), 46 | "qty": each_goods.get_qty(), 47 | "userId": each_goods.create_uid.id, 48 | "views": each_goods.views, 49 | "weight": each_goods.weight 50 | } 51 | return _dict 52 | 53 | def _product_category_dict(self, category_id): 54 | _dict = { 55 | "dateAdd": category_id.create_date, 56 | "dateUpdate": category_id.write_date, 57 | "icon": '', 58 | "id": category_id.id, 59 | "isUse": category_id.is_use, 60 | "key": category_id.key, 61 | "name": category_id.name, 62 | "paixu": category_id.sort or 0, 63 | "pid": category_id.pid.id if category_id.pid else 0, 64 | "type": '', #category_id.category_type, 65 | "userId": category_id.create_uid.id 66 | } 67 | return _dict 68 | 69 | def get_goods_domain(self, category_id, nameLike, **kwargs): 70 | if 'recommendStatus' in kwargs or 'pingtuan' in kwargs or 'kanjia' in kwargs or 'miaosha' in kwargs: 71 | return [('id', '=', 0)] 72 | domain = [('sale_ok', '=', True), ('wxapp_published', '=', True)] 73 | if not category_id and nameLike and nameLike.startswith('_c_'): 74 | _name = nameLike.replace('_c_', '') 75 | category = request.env['wxapp.product.category'].sudo().search([('name', '=', _name)], limit=1) 76 | if category: 77 | category_id = category.id 78 | nameLike = '' 79 | else: 80 | nameLike = _name 81 | if category_id: 82 | cate_ids = [int(category_id)] + request.env['wxapp.product.category'].sudo().browse(int(category_id)).child_ids.ids 83 | #cate_ids = request.env['wxapp.product.category'].sudo().search([('pid', 'child_of', int(category_id))]).ids 84 | domain.append(('wxpp_category_id', 'in', cate_ids)) 85 | if nameLike: 86 | for srch in nameLike.split(" "): 87 | domain += ['|', ('name', 'ilike', srch), '|', ('barcode', '=', srch), ('default_code', '=', srch)] 88 | 89 | return domain 90 | 91 | def get_order_by(self, order_by): 92 | ret = 'wxpp_category_id,sequence' 93 | if order_by=='priceUp': 94 | ret = 'list_price' 95 | elif order_by=='ordersDown': 96 | pass 97 | elif order_by=='addedDown': 98 | ret = 'create_date desc' 99 | elif order_by=='recommendSort': 100 | ret = 'recommend_sort' 101 | return ret 102 | 103 | @http.route(['/wxa//shop/goods/list', '/wxa//shop/goods/list/v2'], auth='public', methods=['GET', 'POST'], csrf=False) 104 | def list(self, sub_domain, categoryId=False, nameLike=False, page=1, pageSize=20, **kwargs): 105 | _logger.info('>>> product list %s', kwargs) 106 | page = int(page) 107 | pageSize = int(pageSize) 108 | category_id = categoryId 109 | token = kwargs.get('token', None) 110 | order_by = kwargs.get('orderBy', None) 111 | if not nameLike: 112 | nameLike = kwargs.get('k') 113 | try: 114 | ret, entry = self._check_domain(sub_domain) 115 | if ret:return ret 116 | self.check_userid(token) 117 | 118 | if 'recommendStatus' in kwargs: 119 | order_by = 'recommendSort' 120 | domain = self.get_goods_domain(category_id, nameLike, **kwargs) 121 | order = self.get_order_by(order_by) 122 | 123 | _logger.info('>>> list domain %s order %s', domain, order) 124 | goods_list = request.env['product.template'].sudo().search(domain, offset=(page-1)*pageSize, limit=pageSize, order=order) 125 | 126 | if not goods_list: 127 | return self.res_err(700) 128 | data = [ self._product_basic_dict(each_goods) for each_goods in goods_list] 129 | if 'recommendStatus' in kwargs: 130 | for index in range(len(goods_list)): 131 | self.product_info_ext(data[index], goods_list[index], None) 132 | all_count = request.env['product.template'].sudo().search_count(domain) 133 | if '/v2' in request.httprequest.url: 134 | return self.res_ok({ 135 | 'result': data, 136 | 'totalPage': (all_count // pageSize) + 1, 137 | 'totalRow': all_count, 138 | }) 139 | else: 140 | data[0]['all_count'] = all_count 141 | return self.res_ok(data) 142 | 143 | except Exception as e: 144 | _logger.exception(e) 145 | return self.res_err(-1, str(e)) 146 | 147 | 148 | def pre_check(self, entry, kwargs): 149 | return 150 | 151 | @http.route('/wxa//shop/goods/detail', auth='public', methods=['GET']) 152 | def detail(self, sub_domain, id=False, code=False, **kwargs): 153 | goods_id = id 154 | token = kwargs.get('token', None) 155 | try: 156 | ret, entry = self._check_domain(sub_domain) 157 | if ret:return ret 158 | self.check_userid(token) 159 | 160 | res = self.pre_check(entry, kwargs) 161 | if res:return res 162 | 163 | if not goods_id and not code: 164 | return self.res_err(300) 165 | 166 | if goods_id: 167 | product = None 168 | goods = request.env['product.template'].sudo().browse(int(goods_id)) 169 | else: 170 | product = request.env['product.product'].sudo().search([('barcode', '=', code)]) 171 | goods = product.product_tmpl_id 172 | 173 | if not goods: 174 | return self.res_err(404) 175 | 176 | if not goods.wxapp_published: 177 | return self.res_err(404) 178 | 179 | description_value = None 180 | is_py2 = sys.version_info.major==2 181 | if goods.description_wxapp: 182 | _content = is_py2 and goods.description_wxapp or str(goods.description_wxapp or '').replace('', '') 183 | if _content: 184 | description_value = is_py2 and goods.description_wxapp or str(goods.description_wxapp or '') 185 | if not description_value: 186 | if hasattr(goods, 'website_description'): 187 | description_value = is_py2 and goods.website_description or str(goods.website_description or '') 188 | 189 | data = { 190 | "code": 0, 191 | "data": { 192 | "category": self._product_category_dict(goods.wxpp_category_id), 193 | "pics": json.loads(goods.images_data), 194 | "content": convert_static_link(request, description_value) if description_value else '', 195 | "basicInfo": self._product_basic_dict(goods) 196 | }, 197 | "msg": "success" 198 | } 199 | self.product_info_ext(data['data'], goods, product) 200 | 201 | # goods.sudo().write({'views': goods.views + 1}) #同时同用户多次重复请求的事务问题 202 | return self.res_ok(data['data']) 203 | 204 | except Exception as e: 205 | _logger.exception(e) 206 | return self.res_err(-1, str(e)) 207 | 208 | def product_info_ext(self, data, goods, product): 209 | data["logistics"] = { 210 | "logisticsBySelf": False, 211 | "isFree": False, 212 | "by_self": False, 213 | "feeType": 0, 214 | "feeTypeStr": '按件', 215 | "details": [] 216 | } 217 | 218 | @http.route('/wxa//shop/goods/reputation', auth='public', methods=['GET', 'POST'], csrf=False) 219 | def reputation(self, sub_domain, goodsId=None, **kwargs): 220 | return self.res_ok([]) 221 | -------------------------------------------------------------------------------- /controllers/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | import odoo 6 | from odoo import http 7 | from odoo.http import request 8 | from odoo import fields 9 | 10 | from .. import defs 11 | from .base import BaseController, jsonapi 12 | from .tools import get_wx_session_info, get_wx_user_info, get_decrypt_info 13 | 14 | import logging 15 | 16 | _logger = logging.getLogger(__name__) 17 | 18 | 19 | class WxappUser(http.Controller, BaseController): 20 | 21 | def after_check(self, wechat_user, token, data): 22 | pass 23 | 24 | def after_login(self, wechat_user, token, data): 25 | pass 26 | 27 | @http.route('/wxa//user/check-token', auth='public', methods=['GET']) 28 | def check_token(self, sub_domain, token=None, **kwargs): 29 | try: 30 | res, wechat_user, entry = self._check_user(sub_domain, token) 31 | if res:return self.res_err(609) 32 | 33 | account_ok = wechat_user.check_account_ok() 34 | if account_ok==True: 35 | data = self.get_user_info(wechat_user) 36 | self.after_check(wechat_user, token, data) 37 | return self.res_ok(data) 38 | else: 39 | if account_ok==-2 or account_ok==-3: 40 | return self.res_err(610, u'需要补充信息') 41 | return self.res_err(608, u'账号不可用') 42 | except Exception as e: 43 | _logger.exception(e) 44 | return self.res_err(-1, str(e)) 45 | 46 | @http.route(['/wxa//user/wxapp/login', '/wxa//user/wxapp/authorize'], auth='public', methods=['GET', 'POST'],csrf=False) 47 | def login(self, sub_domain, code=None, **kwargs): 48 | try: 49 | ret, entry = self._check_domain(sub_domain) 50 | if ret:return ret 51 | 52 | if not code: 53 | return self.res_err(300) 54 | 55 | app_id = entry.get_config('app_id') 56 | secret = entry.get_config('secret') 57 | 58 | if not app_id or not secret: 59 | return self.res_err(404) 60 | 61 | session_info = get_wx_session_info(app_id, secret, code) 62 | if session_info.get('errcode'): 63 | return self.res_err(-1, session_info.get('errmsg')) 64 | 65 | open_id = session_info['openid'] 66 | wechat_user = request.env(user=1)['wxapp.user'].search([ 67 | ('open_id', '=', open_id), 68 | #('create_uid', '=', user.id) 69 | ]) 70 | if not wechat_user: 71 | return self.res_err(10000, {'session_info': session_info}) 72 | 73 | wechat_user.write({'last_login': fields.Datetime.now(), 'ip': request.httprequest.remote_addr}) 74 | access_token = request.env(user=1)['wxapp.access_token'].search([ 75 | ('open_id', '=', open_id), 76 | #('create_uid', '=', user.id) 77 | ]) 78 | 79 | if not access_token: 80 | session_key = session_info['session_key'] 81 | access_token = request.env(user=1)['wxapp.access_token'].create({ 82 | 'open_id': open_id, 83 | 'session_key': session_key, 84 | 'sub_domain': sub_domain, 85 | }) 86 | else: 87 | access_token.write({'session_key': session_info['session_key']}) 88 | 89 | data = { 90 | 'token': access_token.token, 91 | 'uid': wechat_user.id, 92 | 'info': self.get_user_info(wechat_user) 93 | } 94 | self.after_login(wechat_user, access_token.token, data) 95 | return self.res_ok(data) 96 | 97 | except AttributeError: 98 | import traceback;traceback.print_exc() 99 | return self.res_err(404) 100 | 101 | except Exception as e: 102 | _logger.exception(e) 103 | return self.res_err(-1, str(e)) 104 | 105 | 106 | @http.route('/wxa//user/wxapp/register/complex', auth='public', methods=['GET', 'POST'], csrf=False) 107 | def register(self, sub_domain, code=None, encryptedData=None, iv=None, **kwargs): 108 | ''' 109 | 用户注册 110 | ''' 111 | try: 112 | ret, entry = self._check_domain(sub_domain) 113 | if ret:return ret 114 | 115 | session_info = kwargs.get('session_info') 116 | if session_info: 117 | session_info = json.loads(session_info) 118 | user_info = { 119 | 'nickName': '微信用户', 120 | 'openId': session_info.get('openid'), 121 | 'unionId': session_info.get('unionid') 122 | } 123 | else: 124 | encrypted_data = encryptedData 125 | if not code or not encrypted_data or not iv: 126 | return self.res_err(300) 127 | 128 | app_id = entry.get_config('app_id') 129 | secret = entry.get_config('secret') 130 | 131 | if not app_id or not secret: 132 | return self.res_err(404) 133 | 134 | session_key, user_info = get_wx_user_info(app_id, secret, code, encrypted_data, iv) 135 | if kwargs.get('userInfo'): 136 | user_info.update(json.loads(kwargs.get('userInfo'))) 137 | 138 | user_id = None 139 | if hasattr(request, 'user_id'): 140 | user_id = request.user_id 141 | 142 | vals = { 143 | 'name': user_info['nickName'], 144 | 'nickname': user_info['nickName'], 145 | 'open_id': user_info['openId'], 146 | 'gender': user_info.get('gender'), 147 | 'language': user_info.get('language'), 148 | 'country': user_info.get('country'), 149 | 'province': user_info.get('province'), 150 | 'city': user_info.get('city'), 151 | 'avatar_url': user_info.get('avatarUrl'), 152 | 'register_ip': request.httprequest.remote_addr, 153 | 'user_id': user_id, 154 | 'partner_id': user_id and request.env['res.users'].sudo().browse(user_id).partner_id.id or None, 155 | 'category_id': [(4, request.env.ref('oejia_weshop.res_partner_category_data_1').sudo().id)], 156 | 'entry_id': entry.id, 157 | 'customer_rank': 1, 158 | } 159 | if user_id: 160 | vals['user_id'] = user_id 161 | vals['partner_id'] = request.env['res.users'].sudo().browse(user_id).partner_id.id 162 | vals.pop('name') 163 | try: 164 | wechat_user = request.env(user=1)['wxapp.user'].create(vals) 165 | except: 166 | import traceback;traceback.print_exc() 167 | return self.res_err(-99, u'账号状态异常') 168 | wechat_user.action_created(vals) 169 | request.wechat_user = wechat_user 170 | request.entry = entry 171 | return self.res_ok() 172 | 173 | except AttributeError: 174 | return self.res_err(404) 175 | 176 | except Exception as e: 177 | _logger.exception(e) 178 | return self.res_err(-1, str(e)) 179 | 180 | def get_user_info(self, wechat_user): 181 | mobile = '' 182 | if hasattr(wechat_user, 'partner_id'): 183 | mobile = wechat_user.partner_id.mobile 184 | data = { 185 | 'base':{ 186 | 'mobile': mobile or '', 187 | 'userid': '', 188 | }, 189 | } 190 | return data 191 | 192 | def get_user_more(self, wechat_user, entry): 193 | return {} 194 | 195 | @http.route('/wxa//user/detail', auth='public', methods=['GET']) 196 | def detail(self, sub_domain, token=None, **kwargs): 197 | try: 198 | res, wechat_user, entry = self._check_user(sub_domain, token) 199 | if res:return res 200 | 201 | data = self.get_user_info(wechat_user) 202 | data.update(self.get_user_more(wechat_user, entry)) 203 | return self.res_ok(data) 204 | 205 | except Exception as e: 206 | _logger.exception(e) 207 | return self.res_err(-1, str(e)) 208 | 209 | @http.route('/wxa//user/wxapp/bindMobile', auth='public', methods=['GET', 'POST'], csrf=False) 210 | def bind_mobile(self, sub_domain, token=None, encryptedData=None, iv=None, **kwargs): 211 | try: 212 | res, wechat_user, entry = self._check_user(sub_domain, token) 213 | if res and not wechat_user:return res 214 | 215 | encrypted_data = encryptedData 216 | if not token or not encrypted_data or not iv: 217 | return self.res_err(300) 218 | 219 | app_id = entry.get_config('app_id') 220 | secret = entry.get_config('secret') 221 | 222 | if not app_id or not secret: 223 | return self.res_err(404) 224 | 225 | access_token = request.env(user=1)['wxapp.access_token'].search([ 226 | ('token', '=', token), 227 | ]) 228 | if not access_token: 229 | return self.res_err(901) 230 | session_key = access_token[0].session_key 231 | 232 | _logger.info('>>> decrypt: %s %s %s %s', app_id, session_key, encrypted_data, iv) 233 | user_info = get_decrypt_info(app_id, session_key, encrypted_data, iv) 234 | _logger.info('>>> bind_mobile: %s', user_info) 235 | wechat_user.bind_mobile(user_info.get('phoneNumber')) 236 | ret = { 237 | 'account_ok': wechat_user.check_account_ok(), 238 | 'mobile': wechat_user.mobile, 239 | } 240 | return self.res_ok(ret) 241 | 242 | except Exception as e: 243 | _logger.exception(e) 244 | return self.res_err(-1, str(e)) 245 | 246 | @http.route('/wxa//user/amount', auth='public', methods=['GET']) 247 | def user_amount(self, sub_domain, token=None, **kwargs): 248 | try: 249 | res, wechat_user, entry = self._check_user(sub_domain, token) 250 | if res: 251 | if entry and kwargs.get('access_token'): 252 | return self.res_ok({'balance': 0, 'score': 0}) 253 | else: 254 | return res 255 | _data = { 256 | 'balance': wechat_user.get_balance(), 257 | 'creditLimit': wechat_user.get_credit_limit(), 258 | 'freeze': 0, 259 | 'score': wechat_user.get_score(), 260 | 'totleConsumed': 0, 261 | } 262 | self.amount_info_ext(wechat_user, entry, _data, kwargs) 263 | return self.res_ok(_data) 264 | 265 | except Exception as e: 266 | _logger.exception(e) 267 | return self.res_err(-1, str(e)) 268 | 269 | def amount_info_ext(self, wechat_user, entry, info, kwargs): 270 | pass -------------------------------------------------------------------------------- /ext_libs/weixin/helper.py: -------------------------------------------------------------------------------- 1 | # -*-coding: utf-8 -*- 2 | # !/usr/bin/env python 3 | from __future__ import unicode_literals 4 | 5 | """ 6 | File: client.py 7 | Author: goodspeed 8 | Email: cacique1103@gmail.com 9 | Github: https://github.com/zongxiao 10 | Date: 2015-02-11 11 | Description: Weixin helpers 12 | """ 13 | 14 | import sys 15 | import datetime 16 | from hashlib import sha1 17 | from decimal import Decimal 18 | 19 | import six 20 | from six.moves import html_parser 21 | 22 | PY2 = sys.version_info[0] == 2 23 | 24 | _always_safe = (b'abcdefghijklmnopqrstuvwxyz' 25 | b'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-+') 26 | 27 | safe_char = _always_safe 28 | 29 | error_dict = { 30 | 'AppID 参数错误': { 31 | 'errcode': 40013, 32 | 'errmsg': 'invalid appid' 33 | } 34 | } 35 | 36 | 37 | if PY2: 38 | text_type = unicode 39 | iteritems = lambda d, *args, **kwargs: d.iteritems(*args, **kwargs) 40 | 41 | def to_native(x, charset=sys.getdefaultencoding(), errors='strict'): 42 | if x is None or isinstance(x, str): 43 | return x 44 | return x.encode(charset, errors) 45 | else: 46 | text_type = str 47 | iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs)) 48 | 49 | def to_native(x, charset=sys.getdefaultencoding(), errors='strict'): 50 | if x is None or isinstance(x, str): 51 | return x 52 | return x.decode(charset, errors) 53 | 54 | 55 | """ 56 | The md5 and sha modules are deprecated since Python 2.5, replaced by the 57 | hashlib module containing both hash algorithms. Here, we provide a common 58 | interface to the md5 and sha constructors, preferring the hashlib module when 59 | available. 60 | """ 61 | 62 | try: 63 | import hashlib 64 | md5_constructor = hashlib.md5 65 | md5_hmac = md5_constructor 66 | sha_constructor = hashlib.sha1 67 | sha_hmac = sha_constructor 68 | except ImportError: 69 | import md5 70 | md5_constructor = md5.new 71 | md5_hmac = md5 72 | import sha 73 | sha_constructor = sha.new 74 | sha_hmac = sha 75 | 76 | 77 | class Promise(object): 78 | """ 79 | This is just a base class for the proxy class created in 80 | the closure of the lazy function. It can be used to recognize 81 | promises in code. 82 | """ 83 | pass 84 | 85 | 86 | class _UnicodeDecodeError(UnicodeDecodeError): 87 | def __init__(self, obj, *args): 88 | self.obj = obj 89 | UnicodeDecodeError.__init__(self, *args) 90 | 91 | def __str__(self): 92 | original = UnicodeDecodeError.__str__(self) 93 | return '%s. You passed in %r (%s)' % (original, self.obj, 94 | type(self.obj)) 95 | 96 | 97 | def smart_text(s, encoding='utf-8', strings_only=False, errors='strict'): 98 | """ 99 | Returns a text object representing 's' -- unicode on Python 2 and str on 100 | Python 3. Treats bytestrings using the 'encoding' codec. 101 | If strings_only is True, don't convert (some) non-string-like objects. 102 | """ 103 | if isinstance(s, Promise): 104 | # The input is the result of a gettext_lazy() call. 105 | return s 106 | return force_text(s, encoding, strings_only, errors) 107 | 108 | 109 | _PROTECTED_TYPES = six.integer_types + (type(None), float, Decimal, 110 | datetime.datetime, datetime.date, 111 | datetime.time) 112 | 113 | 114 | def is_protected_type(obj): 115 | """Determine if the object instance is of a protected type. 116 | Objects of protected types are preserved as-is when passed to 117 | force_text(strings_only=True). 118 | """ 119 | return isinstance(obj, _PROTECTED_TYPES) 120 | 121 | 122 | def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): 123 | """ 124 | Similar to smart_text, except that lazy instances are resolved to 125 | strings, rather than kept as lazy objects. 126 | If strings_only is True, don't convert (some) non-string-like objects. 127 | """ 128 | # Handle the common case first for performance reasons. 129 | if issubclass(type(s), six.text_type): 130 | return s 131 | if strings_only and is_protected_type(s): 132 | return s 133 | try: 134 | if not issubclass(type(s), six.string_types): 135 | if six.PY3: 136 | if isinstance(s, bytes): 137 | s = six.text_type(s, encoding, errors) 138 | else: 139 | s = six.text_type(s) 140 | elif hasattr(s, '__unicode__'): 141 | s = six.text_type(s) 142 | else: 143 | s = six.text_type(bytes(s), encoding, errors) 144 | else: 145 | # Note: We use .decode() here, instead of six.text_type(s, encoding, 146 | # errors), so that if s is a SafeBytes, it ends up being a 147 | # SafeText at the end. 148 | s = s.decode(encoding, errors) 149 | except UnicodeDecodeError as e: 150 | if not isinstance(s, Exception): 151 | raise _UnicodeDecodeError(s, *e.args) 152 | else: 153 | # If we get to here, the caller has passed in an Exception 154 | # subclass populated with non-ASCII bytestring data without a 155 | # working unicode method. Try to handle this without raising a 156 | # further exception by individually forcing the exception args 157 | # to unicode. 158 | s = ' '.join(force_text(arg, encoding, strings_only, errors) 159 | for arg in s) 160 | return s 161 | 162 | 163 | def smart_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): 164 | """ 165 | Returns a bytestring version of 's', encoded as specified in 'encoding'. 166 | If strings_only is True, don't convert (some) non-string-like objects. 167 | """ 168 | if isinstance(s, Promise): 169 | # The input is the result of a gettext_lazy() call. 170 | return s 171 | return force_bytes(s, encoding, strings_only, errors) 172 | 173 | 174 | def force_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): 175 | """ 176 | Similar to smart_bytes, except that lazy instances are resolved to 177 | strings, rather than kept as lazy objects. 178 | If strings_only is True, don't convert (some) non-string-like objects. 179 | """ 180 | # Handle the common case first for performance reasons. 181 | if isinstance(s, bytes): 182 | if encoding == 'utf-8': 183 | return s 184 | else: 185 | return s.decode('utf-8', errors).encode(encoding, errors) 186 | if strings_only and is_protected_type(s): 187 | return s 188 | if isinstance(s, Promise): 189 | return six.text_type(s).encode(encoding, errors) 190 | if not isinstance(s, six.string_types): 191 | try: 192 | if six.PY3: 193 | return six.text_type(s).encode(encoding) 194 | else: 195 | return bytes(s) 196 | except UnicodeEncodeError: 197 | if isinstance(s, Exception): 198 | # An Exception subclass containing non-ASCII data that doesn't 199 | # know how to print itself properly. We shouldn't raise a 200 | # further exception. 201 | return b' '.join(force_bytes(arg, encoding, 202 | strings_only, errors) 203 | for arg in s) 204 | return six.text_type(s).encode(encoding, errors) 205 | else: 206 | return s.encode(encoding, errors) 207 | 208 | if six.PY3: 209 | smart_str = smart_text 210 | force_str = force_text 211 | else: 212 | smart_str = smart_bytes 213 | force_str = force_bytes 214 | # backwards compatibility for Python 2 215 | smart_unicode = smart_text 216 | force_unicode = force_text 217 | 218 | smart_str.__doc__ = """ 219 | Apply smart_text in Python 3 and smart_bytes in Python 2. 220 | This is suitable for writing to sys.stdout (for instance). 221 | """ 222 | 223 | force_str.__doc__ = """ 224 | Apply force_text in Python 3 and force_bytes in Python 2. 225 | """ 226 | 227 | 228 | def genarate_js_signature(params): 229 | keys = params.keys() 230 | keys.sort() 231 | params_str = b'' 232 | for key in keys: 233 | params_str += b'%s=%s&' % (smart_str(key), smart_str(params[key])) 234 | params_str = params_str[:-1] 235 | return sha1(params_str).hexdigest() 236 | 237 | 238 | def genarate_signature(params): 239 | sorted_params = sorted([v for k, v in params.items()]) 240 | params_str = smart_str(''.join(sorted_params)) 241 | return sha1(str(params_str).encode('utf-8')).hexdigest() 242 | 243 | 244 | def get_encoding(html=None, headers=None): 245 | try: 246 | import chardet 247 | if html: 248 | encoding = chardet.detect(html).get('encoding') 249 | return encoding 250 | except ImportError: 251 | pass 252 | if headers: 253 | content_type = headers.get('content-type') 254 | try: 255 | encoding = content_type.split(' ')[1].split('=')[1] 256 | return encoding 257 | except IndexError: 258 | pass 259 | 260 | 261 | def iter_multi_items(mapping): 262 | """ 263 | Iterates over the items of a mapping yielding keys and values 264 | without dropping any from more complex structures. 265 | """ 266 | if isinstance(mapping, dict): 267 | for key, value in iteritems(mapping): 268 | if isinstance(value, (tuple, list)): 269 | for value in value: 270 | yield key, value 271 | else: 272 | yield key, value 273 | else: 274 | for item in mapping: 275 | yield item 276 | 277 | 278 | def url_quote(string, charset='utf-8', errors='strict', safe='/:', unsafe=''): 279 | """ 280 | URL encode a single string with a given encoding. 281 | 282 | :param s: the string to quote. 283 | :param charset: the charset to be used. 284 | :param safe: an optional sequence of safe characters. 285 | :param unsafe: an optional sequence of unsafe characters. 286 | 287 | .. versionadded:: 0.9.2 288 | The `unsafe` parameter was added. 289 | """ 290 | if not isinstance(string, (text_type, bytes, bytearray)): 291 | string = text_type(string) 292 | if isinstance(string, text_type): 293 | string = string.encode(charset, errors) 294 | if isinstance(safe, text_type): 295 | safe = safe.encode(charset, errors) 296 | if isinstance(unsafe, text_type): 297 | unsafe = unsafe.encode(charset, errors) 298 | safe = frozenset(bytearray(safe) + _always_safe) - frozenset(bytearray(unsafe)) 299 | rv = bytearray() 300 | for char in bytearray(string): 301 | if char in safe: 302 | rv.append(char) 303 | else: 304 | rv.extend(('%%%02X' % char).encode('ascii')) 305 | return to_native(bytes(rv)) 306 | 307 | 308 | def url_quote_plus(string, charset='utf-8', errors='strict', safe=''): 309 | return url_quote(string, charset, errors, safe + ' ', '+').replace(' ', '+') 310 | 311 | 312 | def _url_encode_impl(obj, charset, encode_keys, sort, key): 313 | iterable = iter_multi_items(obj) 314 | if sort: 315 | iterable = sorted(iterable, key=key) 316 | for key, value in iterable: 317 | if value is None: 318 | continue 319 | if not isinstance(key, bytes): 320 | key = text_type(key).encode(charset) 321 | if not isinstance(value, bytes): 322 | value = text_type(value).encode(charset) 323 | yield url_quote_plus(key) + '=' + url_quote_plus(value) 324 | 325 | 326 | def url_encode(obj, charset='utf-8', encode_keys=False, sort=False, key=None, 327 | separator=b'&'): 328 | separator = to_native(separator, 'ascii') 329 | return separator.join(_url_encode_impl(obj, charset, encode_keys, sort, key)) 330 | 331 | 332 | class WeixiErrorParser(html_parser.HTMLParser): 333 | 334 | def __init__(self): 335 | html_parser.HTMLParser.__init__(self) 336 | self.recording = 0 337 | self.data = [] 338 | 339 | def handle_starttag(self, tag, attrs): 340 | if tag != 'h4': 341 | return 342 | if self.recording: 343 | self.recording += 1 344 | self.recording = 1 345 | 346 | def handle_endtag(self, tag): 347 | if tag == 'h4' and self.recording: 348 | self.recording -= 1 349 | 350 | def handle_data(self, data): 351 | if self.recording: 352 | self.data.append(data) 353 | 354 | 355 | def error_parser(error_html, encoding='gbk'): 356 | html = text_type(error_html, encoding or 'gbk') 357 | error_parser = WeixiErrorParser() 358 | error_parser.feed(html) 359 | if error_parser.data: 360 | return error_dict.get(error_parser.data[0], None) 361 | 362 | 363 | def validate_xml(xml): 364 | """ 365 | 使用lxml.etree.parse 检测xml是否符合语法规范 366 | """ 367 | from lxml import etree 368 | try: 369 | return etree.parse(xml) 370 | except etree.XMLSyntaxError: 371 | return False 372 | --------------------------------------------------------------------------------
16 | 项目主页: https://github.com/JoneXiong/oejia_weshop 17 |
19 | 官网网站: www.oejia.net 20 |
22 | 文档说明: oejia_weshop_document.html 23 |
25 | 联 系: 微信 johan-x | Q 669229467 | Email odoo@calluu.com | Q群 260160505 26 |
85 | 暂无订单 86 |