├── 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 | 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 |
13 |
16 | 17 |
18 |
19 | 20 | wxapp.confirm.view_form_send 21 | wxapp.confirm 22 | form 23 | 24 |
25 | 26 |
27 |
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 | ![Odoo小程序商城](https://raw.githubusercontent.com/JoneXiong/oejia_weshop/master/static/description/odoo_wxapp.jpg) 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 | ![用户管理](http://oejia.net/files/201809/13165725703.jpeg) 40 | 41 | ![产品管理](http://oejia.net/files/201809/13172849146.jpeg) 42 | 43 | ![对接配置](http://oejia.net/files/201809/13165316092.jpeg) 44 | 45 | ![小程序客户端](http://oejia.net/files/201809/13172406513.jpeg) 46 | 47 | 48 | ![我的订单](http://oejia.net/files/201809/13172524213.jpeg) 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 |
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 | 49 | 50 | """ 51 | 52 | THUM_MUSIC_TEMPLATE = """ 53 | 54 | 55 | 56 | {create_time} 57 | 58 | 59 | <![CDATA[{title}]]> 60 | 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | 68 | NOTHUM_MUSIC_TEMPLATE = """ 69 | 70 | 71 | 72 | {create_time} 73 | 74 | 75 | <![CDATA[{title}]]> 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 | <![CDATA[{title}]]> 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 |
19 |
22 |
23 |
24 |
25 |
26 | 27 | sale.order.form_weshop_inherit 28 | sale.order 29 | 30 | form 31 | 32 | 33 | 34 | 35 |