diff --git a/pos_wechat_miniprogram/README.rst b/pos_wechat_miniprogram/README.rst index 23fadb904..aeeb01a6c 100644 --- a/pos_wechat_miniprogram/README.rst +++ b/pos_wechat_miniprogram/README.rst @@ -4,6 +4,74 @@ Integrate POS with WeChat mini-program +Verification mobile number +========================== + +Quick +----- + +Use the mobile phone number specified in your WeChat account.:: + + authByWeChat: function (e) { + var detail = e.detail; + var params = { + model: 'res.users', + method: 'wechat_mobile_number_verification', + args: [detail], + context: {}, + kwargs: {} + }; + odooRpc(params).then(function (res) { + wx.setStorageSync('telephoneNumberVerified', res.result); + }) + } + +With code confirmation +---------------------- + +You need to enter a phone number and confirm the number using a code from SMS message.:: + + submitNumber: function(e) { + var value = e.detail.value; + var TemplateID = 1; # id of verification template (model 'qcloud.sms.template') + var params = { + model: 'res.users', + method: 'template_sms_mobile_number_verification', + args: [value.usrtel, TemplateID], + context: {}, + kwargs: {} + }; + odooRpc(params).then(function (res) { + # your code here ... + }); + }, + submitCode: function (e) { + var value = e.detail.value; + var params = { + model: 'res.users', + method: 'check_verification_code', + args: [value.code], + context: {}, + kwargs: {} + }; + odooRpc(params).then(function (res) { + wx.setStorageSync('telephoneNumberVerified', res.result); + }); + } + +Payments +======== + +Pay via WeChat mini-program +--------------------------- + +TODO + +Pay via POS +----------- + +TODO + Credits ======= diff --git a/pos_wechat_miniprogram/__init__.py b/pos_wechat_miniprogram/__init__.py index de9559250..b239393e2 100644 --- a/pos_wechat_miniprogram/__init__.py +++ b/pos_wechat_miniprogram/__init__.py @@ -1 +1,4 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from . import models +from . import wizard +from . import report diff --git a/pos_wechat_miniprogram/__manifest__.py b/pos_wechat_miniprogram/__manifest__.py index 06404c042..215d97e1d 100644 --- a/pos_wechat_miniprogram/__manifest__.py +++ b/pos_wechat_miniprogram/__manifest__.py @@ -18,12 +18,26 @@ "depends": [ "wechat_miniprogram", + "qcloud_sms", + "pos_multi_session_restaurant", + "pos_order_note", "pos_wechat", + "base_geolocalize", ], "external_dependencies": {"python": [], "bin": []}, "data": [ "security/wechat_security.xml", "security/ir.model.access.csv", + "views/product_view.xml", + "views/pos_wechat_miniprogram_view.xml", + "views/template.xml", + "views/pos_config_view.xml", + "views/pos_restaurant_view.xml", + "views/pos_multi_session_restaurant_view.xml", + "views/res_config_settings_view.xml", + "wizard/qrcode.xml", + "wizard/pos_payment_views.xml", + "report/report_table_qrcode.xml", ], "demo": [ ], diff --git a/pos_wechat_miniprogram/doc/index.rst b/pos_wechat_miniprogram/doc/index.rst index 889115206..43a9a98d3 100644 --- a/pos_wechat_miniprogram/doc/index.rst +++ b/pos_wechat_miniprogram/doc/index.rst @@ -8,3 +8,42 @@ Installation ============ * `Install `__ this module in a usual way + +Configuration +============= + +WeChat mini-program Journals +---------------------------- + +WeChat mini-program Journals are created automatically on first opening new POS session. + +* In demo installation: they are availabe in POS immediatly +* In non-demo installation: add Journals to **Payment Methods** in *Point of + Sale*'s Settings, then close existing session if any and open again + +Usage +===== + +Generate mini-program QR Codes for tables of restaurant +------------------------------------------------------- + +* Go to ``[[ Point of Sale ]] >> WeChat mini-program >> QR Code`` +* Specify ``Floor`` +* Specify ``Quantity`` - quantity of QR codes for each table +* Specify ``Tables`` of the ``Floor`` +* Click on ``[Print]`` +* RESULT: You will get a PDF report with QR codes. + +Usage of POS +------------ +* Go to ``[[ Point of Sale ]] >> Configuration >> Point of Sale`` +* Open ``POS`` form +* Click on ``[Edit]`` +* Specify ``Allow receiving messages`` +* Specify ``Auto Print miniprogram Orders`` +* Click on ``[Save]`` +* Open ``POS`` session +* Create ``Order`` from WeChat mini-program and pay via mini-program +* Click ``Validate`` in the POS for confirming the ``Order`` +* Create new ``Order`` from WeChat mini-program without pay +* Click ``Payment`` in the POS for payment the ``Order`` diff --git a/pos_wechat_miniprogram/models/__init__.py b/pos_wechat_miniprogram/models/__init__.py new file mode 100644 index 000000000..a583b7a1a --- /dev/null +++ b/pos_wechat_miniprogram/models/__init__.py @@ -0,0 +1,11 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from . import product +from . import user +from . import http +from . import wechat_order +from . import pos_config +from . import pos_order +from . import pos_wechat_miniprogram +from . import pos_restaurant +from . import pos_multi_session_models +from . import res_config_settings diff --git a/pos_wechat_miniprogram/models/http.py b/pos_wechat_miniprogram/models/http.py new file mode 100644 index 000000000..759f7d303 --- /dev/null +++ b/pos_wechat_miniprogram/models/http.py @@ -0,0 +1,14 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import models +from odoo.http import request + + +class Http(models.AbstractModel): + _inherit = 'ir.http' + + def session_info(self): + result = super(Http, self).session_info() + user = request.env.user + result['number_verified'] = user.partner_id.number_verified + return result diff --git a/pos_wechat_miniprogram/models/pos_config.py b/pos_wechat_miniprogram/models/pos_config.py new file mode 100644 index 000000000..9b783f8b8 --- /dev/null +++ b/pos_wechat_miniprogram/models/pos_config.py @@ -0,0 +1,58 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import models, fields, api + +MODULE = 'pos_wechat_miniprogram' + + +class PosConfig(models.Model): + _inherit = 'pos.config' + + allow_message_from_miniprogram = fields.Boolean(string='Allow receiving messages', + help='Allow receiving messages from the WeChat mini-program', + default=True) + auto_print_miniprogram_orders = fields.Boolean(string='Auto Print miniprogram Orders', + help='Auto Print miniprogram order to kitchen', + default=True) + shop_id = fields.Many2one(related='multi_session_id.shop_id') + + @api.multi + def open_session_cb(self): + res = super(PosConfig, self).open_session_cb() + self.init_pos_wechat_miniprogram_journal() + return res + + def init_pos_wechat_miniprogram_journal(self): + """Init demo Journals for current company""" + # Multi-company is not primary task for this module, but I copied this + # code from pos_debt_notebook, so why not + journal_obj = self.env['account.journal'] + user = self.env.user + wechat_jsapi_journal_active = journal_obj.search([ + ('company_id', '=', user.company_id.id), + ('wechat', '=', 'jsapi'), + ]) + if wechat_jsapi_journal_active: + return + demo_is_on = self.env['ir.module.module'].search([('name', '=', MODULE)]).demo + + options = { + 'noupdate': True, + 'type': 'cash', + 'write_statement': demo_is_on, + } + wechat_jsapi_journal = self._create_wechat_journal(dict( + sequence_name='Wechat JSAPI Payment', + prefix='WMPJSAPI-- ', + journal_name='Wechat JSAPI Payment', + code='WMPJSAPI', + wechat='jsapi', + **options + )) + + if demo_is_on: + self.write({ + 'journal_ids': [ + (4, wechat_jsapi_journal.id), + ], + }) diff --git a/pos_wechat_miniprogram/models/pos_multi_session_models.py b/pos_wechat_miniprogram/models/pos_multi_session_models.py new file mode 100644 index 000000000..68aa3dcaa --- /dev/null +++ b/pos_wechat_miniprogram/models/pos_multi_session_models.py @@ -0,0 +1,10 @@ +# Copyright 2019 Gabbasov Dinar +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import models, fields + + +class PosMultiSession(models.Model): + _inherit = 'pos.multi_session' + + shop_id = fields.Many2one('res.partner', string='Shop') diff --git a/pos_wechat_miniprogram/models/pos_order.py b/pos_wechat_miniprogram/models/pos_order.py new file mode 100644 index 000000000..488b2617d --- /dev/null +++ b/pos_wechat_miniprogram/models/pos_order.py @@ -0,0 +1,26 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import models, api + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + @api.model + def create_from_ui(self, orders): + res = super(PosOrder, self).create_from_ui(orders) + for o in orders: + data = o.get('data') + miniprogram_data = data.get("miniprogram_order") or {} + submitted_references = data.get('name') + order = self.search([('pos_reference', '=', submitted_references)]) + if order and miniprogram_data.get('id'): + miniprogram_order = self.env['pos.miniprogram.order'].browse(int(miniprogram_data.get('id'))) + if miniprogram_order: + miniprogram_order.write({ + 'state': 'done', + 'order_id': order.id, + 'confirmed_from_pos': True, + 'order_ref': data.get('miniprogram_order_ref') + }) + return res diff --git a/pos_wechat_miniprogram/models/pos_restaurant.py b/pos_wechat_miniprogram/models/pos_restaurant.py new file mode 100644 index 000000000..4b66ca62e --- /dev/null +++ b/pos_wechat_miniprogram/models/pos_restaurant.py @@ -0,0 +1,28 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import api, models, fields +import werkzeug.urls + + +class RestaurantTable(models.Model): + _inherit = 'restaurant.table' + + wechat_miniprogram_qr = fields.Binary(string='WeChat mini-program QR', attachment=True) + + @api.multi + def get_miniprogram_qr_code(self, access_token=False): + self.ensure_one() + if not self.wechat_miniprogram_qr: + param = { + 'floor_id': self.floor_id, + 'table_id': self.id + } + data = { + "path": '%s?%s' % ('pages/index/index', werkzeug.urls.url_encode(param)), + "width": 430 + } + res = self.env['ir.config_parameter'].sudo().get_qr_code(data, access_token) + self.wechat_miniprogram_qr = res + return res + + return self.wechat_miniprogram_qr diff --git a/pos_wechat_miniprogram/models/pos_wechat_miniprogram.py b/pos_wechat_miniprogram/models/pos_wechat_miniprogram.py new file mode 100644 index 000000000..3ece4cb01 --- /dev/null +++ b/pos_wechat_miniprogram/models/pos_wechat_miniprogram.py @@ -0,0 +1,176 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import models, fields, api, _ +import logging +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +CHANNEL_NAME = "wechat.miniprogram" + + +class PosWeChatMiniProgramOrder(models.Model): + """Records with order information and payment status from WeChat mini-program. + + Can be used for sync between mini-programs. """ + + _name = 'pos.miniprogram.order' + _description = "Orders from WeChat mini-program" + _order = 'id desc' + _rec_name = 'date_order' + + name = fields.Char('Name', readonly=True, copy=False) + company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, + default=lambda self: self.env.user.company_id) + date_order = fields.Datetime(string='Order Date', readonly=True, index=True, default=fields.Datetime.now) + partner_id = fields.Many2one('res.partner', string='Customer', index=True, states={'draft': [('readonly', False)]}) + amount_total = fields.Float(compute='_compute_amount_all', string='Total', digits=0) + lines_ids = fields.One2many('pos.miniprogram.order.line', 'order_id', string='Order Lines', + readonly=True, copy=True) + order_id = fields.Many2one('pos.order', string="Point of Sale") + order_ref = fields.Char('Order Reference', readonly=True) + wechat_order_id = fields.Many2one('wechat.order', string="WeChar Order") + state = fields.Selection([ + ('draft', 'Unpaid'), + ('done', 'Paid'), + ('error', 'Error'), + ('refunded', 'Refunded (part of full amount)'), + ], string='State', default='draft') + note = fields.Text(string='Order Notes') + table_id = fields.Many2one('restaurant.table', string='Table', help='The table where this order was served') + floor_id = fields.Many2one('restaurant.floor', string='Floor') + customer_count = fields.Integer(string='Guests', + help='The amount of customers that have been served by this order.') + pay_method = fields.Selection([ + ('now', 'Pay from mini-program'), + ('later', 'Pay from POS') + ], string='Pay method', default='now') + to_invoice = fields.Boolean(string="Invoice", default=False) + confirmed_from_pos = fields.Boolean(string="Order Confirmed from POS", default=False) + user_id = fields.Many2one(related='order_id.user_id', store=True) + packingMethods = fields.Selection([ + ('takeout', 'Takeout'), + ('indoors', 'Indoors'), + ('delivery', 'Delivery') + ], string='Packing method') + shop_id = fields.Many2one('res.partner', string='Shop', index=True, states={'draft': [('readonly', False)]}) + + @api.depends('lines_ids.amount_total', 'lines_ids.discount') + def _compute_amount_all(self): + for order in self: + order.amount_total = sum(line.amount_total for line in order.lines_ids) + + @api.model + def get_user_pos_miniprogram_orders(self): + orders = self.search([('partner_id', '=', self.env.user.partner_id.id)]) + return [o._prepare_mp_message() for o in orders] + + @api.model + def create_from_miniprogram_ui(self, lines, create_vals): + """ + Create order from mini-program and send the order to POS + + :param lines: orderlines from mini-program + :param create_vals: additional information about order + """ + _logger.debug('Create Order from WeChat mini-program: lines - %s, create values - %s', lines, create_vals) + + if self.env.user.number_verified is False: + raise UserError(_("Mobile phone number not specified for User: %s (id: %s)") + % (self.env.user.name, self.env.user.id)) + vals = { + 'lines_ids': [(0, 0, data) for data in lines] + } + + create_vals['partner_id'] = self.env.user.partner_id.id + + if create_vals: + vals.update(create_vals) + + order = self.sudo().create(vals) + + _logger.debug('Mini-program Order: %s', order) + _logger.debug('Order Pay method: %s', order.pay_method) + + if order.pay_method == 'now': + create_vals['miniprogram_order_ids'] = [(4, order.id)] + return self.env['wechat.order'].create_jsapi_order(lines, create_vals) + + order._send_message_to_pos() + return order + + @api.multi + def _update_order_state(self): + self.ensure_one() + self.write({ + 'state': self.wechat_order_id.state + }) + + @api.multi + def on_notification_wechat_order(self): + for r in self: + r._update_order_state() + if r.state == 'done': + r._send_message_to_pos() + + @api.multi + def _prepare_mp_message(self): + """ + To prepare the message of mini-program + """ + self.ensure_one() + res = self.read()[0] + res['lines_ids'] = self.lines_ids.read() + _logger.debug('Read order and orderline: %s', res) + return res + + @api.multi + def _send_message_to_pos(self): + self.ensure_one() + message = self._prepare_mp_message() + for pos in self.env['pos.config'].search([('allow_message_from_miniprogram', '=', True), ('shop_id', '=', self.shop_id.id)]): + self.env['pos.config']._send_to_channel_by_id(self._cr.dbname, pos.id, CHANNEL_NAME, message) + + @api.model + def add_orderline(self, vals): + line = self.env['pos.miniprogram.order.line'].create(vals) + # TODO: send the line to other mini-programs + return line + + @api.model + def remove_orderline(self, id): + line = self.env['pos.miniprogram.order.line'].browse(id) + # TODO: send remove information about the line to other mini-programs + return line.unlink() + + @api.model + def change_orderline(self, id, vals): + line = self.env['pos.miniprogram.order.line'].browse(id) + # TODO: send update information about the line to other mini-programs + return line.write(vals) + + +class PosWeChatMiniProgramOrderLine(models.Model): + _name = 'pos.miniprogram.order.line' + _description = "Lines of WeChat mini-programs Orders" + _rec_name = "product_id" + + name = fields.Char(string='Name', readonly=True, copy=False) + product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], + required=True, change_default=True) + price = fields.Float('Price', required=True, help='Price in currency units (not cents)') + quantity = fields.Float('Quantity', default=1) + order_id = fields.Many2one('pos.miniprogram.order', string="WeChat mini-program Order") + discount = fields.Float(string='Discount (%)', digits=0, default=0.0) + create_date = fields.Datetime(string='Creation Date', readonly=True) + amount_total = fields.Float(compute='_compute_amount_all', string='Total', digits=0) + note = fields.Text(string='Note') + + @api.depends('price', 'quantity', 'discount', 'product_id') + def _compute_amount_all(self): + for line in self: + price = line.price * (1 - (line.discount or 0.0) / 100.0) + amount_total = price * line.quantity + line.update({ + 'amount_total': amount_total, + }) diff --git a/pos_wechat_miniprogram/models/product.py b/pos_wechat_miniprogram/models/product.py new file mode 100644 index 000000000..9e46be8cb --- /dev/null +++ b/pos_wechat_miniprogram/models/product.py @@ -0,0 +1,11 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + hot_product = fields.Boolean(string='Hot Product', help='Check if you this product is hot product (promotion)', + default=False) + banner = fields.Binary(string='Hot product banner', attachment=True) diff --git a/pos_wechat_miniprogram/models/res_config_settings.py b/pos_wechat_miniprogram/models/res_config_settings.py new file mode 100644 index 000000000..61e149fdc --- /dev/null +++ b/pos_wechat_miniprogram/models/res_config_settings.py @@ -0,0 +1,32 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import fields, models, api +from ast import literal_eval + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + sms_verification_template = fields.Many2one('qcloud.sms.template', string='SMS Verification Template', + help='Used to verify a user with a text SMS message') + + @api.multi + def set_values(self): + super(ResConfigSettings, self).set_values() + set_param = self.env['ir.config_parameter'].sudo().set_param + set_param('qcloud.sms_template_id', repr(self.sms_verification_template.id)) + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + get_param = self.env['ir.config_parameter'].sudo().get_param + + sms_template_id = literal_eval(get_param('qcloud.sms_template_id', default='False')) + if sms_template_id and not self.env['qcloud.sms.template'].sudo().browse(sms_template_id).exists(): + sms_template_id = False + + res.update( + sms_verification_template=sms_template_id, + ) + return res diff --git a/pos_wechat_miniprogram/models/user.py b/pos_wechat_miniprogram/models/user.py new file mode 100644 index 000000000..7adcf59e9 --- /dev/null +++ b/pos_wechat_miniprogram/models/user.py @@ -0,0 +1,166 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import api, fields, models, _ +import random +import logging +from datetime import timedelta, datetime + +_logger = logging.getLogger(__name__) + +try: + import phonenumbers +except ImportError as err: + _logger.debug(err) + + +class Users(models.Model): + _inherit = "res.users" + + @api.model + def sms_mobile_number_verification(self, number, **kwargs): + return self.env.user.partner_id._sms_mobile_number_verification(number, **kwargs) + + @api.model + def template_sms_mobile_number_verification(self, number, **kwargs): + template_id = self.env['ir.config_parameter'].sudo().get_param('qcloud.sms_template_id') + return self.env.user.partner_id._template_sms_mobile_number_verification(number, template_id, **kwargs) + + @api.model + def wechat_mobile_number_verification(self, data): + return self.env.user.partner_id._wechat_mobile_number_verification(data) + + @api.model + def check_verification_code(self, code): + return self.env.user.partner_id._check_verification_code(code) + + +class ResPartner(models.Model): + _inherit = "res.partner" + + number_verified = fields.Boolean(string='Verified', default=False) + verification_code = fields.Char(string='Verification code') + end_verification_code_datetime = fields.Datetime(string='Datetime of Verification code') + + @api.multi + def _sms_mobile_number_verification(self, number, **kwargs): + """ + Send verification code to mobile number + + :param number: mobile number from mini-program + :return result: result of send + """ + self.ensure_one() + Qcloud = self.env['qcloud.sms'] + code = random.randrange(10000, 1000000) + duration = random.randrange(2, 6) + + country = Qcloud._get_country(self) + country_code = country.code if country else None + + phone_obj = phonenumbers.parse(number, region=country_code, keep_raw_input=True) + phone_number = phonenumbers.format_number(phone_obj, phonenumbers.PhoneNumberFormat.INTERNATIONAL) + + self.write({ + 'mobile': phone_number, + }) + + message = _("Your verification code is %s, please enter it within %s minutes. For account safety, " + "don't forward the code to others.") % (code, duration) + + result = Qcloud.send_message(message, self.id, **kwargs) + + sms = Qcloud.browse(result.get('sms_id')) + + if sms.state == 'sent': + self.write({ + 'verification_code': code, + 'end_verification_code_datetime': fields.datetime.now() + timedelta(minutes=duration) + }) + + return result + + @api.multi + def _template_sms_mobile_number_verification(self, number, template_id, **kwargs): + """ + Send verification code to mobile number with template of sms + + :param number: mobile number from mini-program + :param template_id: sms template id + :return result: result of send + """ + self.ensure_one() + Qcloud = self.env['qcloud.sms'] + QcloudTemplate = self.env['qcloud.sms.template'].browse(int(template_id)) + code = random.randrange(10000, 1000000) + duration = random.randrange(2, 6) + country = Qcloud._get_country(self) + country_code = country.code if country else None + + phone_obj = phonenumbers.parse(number, region=country_code, keep_raw_input=True) + phone_number = phonenumbers.format_number(phone_obj, phonenumbers.PhoneNumberFormat.INTERNATIONAL) + + self.write({ + 'mobile': phone_number, + }) + + params = str(code) + ',' + str(duration) + + result = QcloudTemplate._send_template_message(self.id, params=params, **kwargs) + sms = Qcloud.browse(result.get('sms_id')) + + if sms.state == 'sent': + self.write({ + 'verification_code': code, + 'end_verification_code_datetime': fields.datetime.now() + timedelta(minutes=duration) + }) + + return result + + @api.multi + def _wechat_mobile_number_verification(self, data): + """ + Save WeChat mobile number + + :param data: data['encryptedData'] Encrypted data with complete user information including sensitive data + :param data: data['iv'] Initial vector of the encryption algorithm + :return result: result of wechat phone number verification + """ + self.ensure_one() + + encryptedData = data.get('encryptedData') + iv = data.get('iv') + session_key = self.wechat_session_key + res = self.env['ir.config_parameter'].sudo().decrypt_wechat_miniprogram_data(session_key, encryptedData, iv) + PhoneNumber = res.get('phoneNumber') + Qcloud = self.env['qcloud.sms'] + country = Qcloud._get_country(self) + country_code = country.code if country else None + phone_obj = phonenumbers.parse(PhoneNumber, region=country_code, keep_raw_input=True) + phone_number = phonenumbers.format_number(phone_obj, phonenumbers.PhoneNumberFormat.INTERNATIONAL) + + self.write({ + 'mobile': phone_number, + 'number_verified': True + }) + + return {'result': True} + + @api.multi + def _check_verification_code(self, code): + """ + :param code: verification code from a sms + :return result: verification result + """ + self.ensure_one() + if int(self.verification_code) == int(code): + now = fields.datetime.now() + end = datetime.strptime(self.end_verification_code_datetime, "%Y-%m-%d %H:%M:%S") + if now > end: + return {'result': False, 'message': _('Verification Code validity is over')} + + self.write({ + 'number_verified': True + }) + return {'result': True} + else: + return {'result': False, 'message': _('Verification Code does not match')} diff --git a/pos_wechat_miniprogram/models/wechat_order.py b/pos_wechat_miniprogram/models/wechat_order.py new file mode 100644 index 000000000..1fc5c6eb7 --- /dev/null +++ b/pos_wechat_miniprogram/models/wechat_order.py @@ -0,0 +1,18 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import models, fields + +CHANNEL_NAME = "wechat.miniprogram" + + +class WeChatOrder(models.Model): + _inherit = 'wechat.order' + + miniprogram_order_ids = fields.One2many('pos.miniprogram.order', 'wechat_order_id', + string='Order from mini-program', readonly=True, copy=False) + + def on_notification(self, data): + order = super(WeChatOrder, self).on_notification(data) + if order.miniprogram_order_ids: + order.miniprogram_order_ids.on_notification_wechat_order() + return order diff --git a/pos_wechat_miniprogram/report/__init__.py b/pos_wechat_miniprogram/report/__init__.py new file mode 100644 index 000000000..49dc9fa78 --- /dev/null +++ b/pos_wechat_miniprogram/report/__init__.py @@ -0,0 +1,2 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from . import report_table_qrcode diff --git a/pos_wechat_miniprogram/report/report_table_qrcode.py b/pos_wechat_miniprogram/report/report_table_qrcode.py new file mode 100644 index 000000000..b86c06c58 --- /dev/null +++ b/pos_wechat_miniprogram/report/report_table_qrcode.py @@ -0,0 +1,32 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import api, models, _ +import logging +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class ReportTableQrCode(models.AbstractModel): + """Abstract Model for report template. + for `_name` model, please use `report.` as prefix then add `module_name.report_name`. + """ + _name = 'report.pos_wechat_miniprogram.report_generate_qr_code' + + @api.model + def get_report_values(self, docids, data=None): + access_token = self.env['ir.config_parameter'].sudo().get_miniprogram_access_token() + _logger.debug('access_token: %s', access_token) + if not access_token: + raise UserError(_('Failed to get access token')) + form = data.get('form') + records = self.env['restaurant.table'].browse(form.get('table_ids')) + docs = [{'name': r.name, 'qr_code': r.get_miniprogram_qr_code(access_token)} for r in records] + + return { + 'doc_ids': data['ids'], + 'doc_model': data['model'], + 'quantity': int(form.get('quantity')), + 'floor': form.get('name'), + 'docs': docs, + } diff --git a/pos_wechat_miniprogram/report/report_table_qrcode.xml b/pos_wechat_miniprogram/report/report_table_qrcode.xml new file mode 100644 index 000000000..740e45f7e --- /dev/null +++ b/pos_wechat_miniprogram/report/report_table_qrcode.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/pos_wechat_miniprogram/security/ir.model.access.csv b/pos_wechat_miniprogram/security/ir.model.access.csv index ddecde45a..3d8b8078f 100644 --- a/pos_wechat_miniprogram/security/ir.model.access.csv +++ b/pos_wechat_miniprogram/security/ir.model.access.csv @@ -1,2 +1,12 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_pos_category,access_pos_category,point_of_sale.model_pos_category,wechat_miniprogram.group_miniprogram_user,1,0,0,0 +access_product_attribute_line,access_product_attribute_line,product.model_product_attribute_line,wechat_miniprogram.group_miniprogram_user,1,0,0,0 +access_product_attribute_price,access_product_attribute_price,product.model_product_attribute_price,wechat_miniprogram.group_miniprogram_user,1,0,0,0 +access_product_attribute_value,access_product_attribute_value,product.model_product_attribute_value,wechat_miniprogram.group_miniprogram_user,1,0,0,0 +access_product_attribute,access_product_attribute,product.model_product_attribute,wechat_miniprogram.group_miniprogram_user,1,0,0,0 +access_pos_miniprogram_order,access_pos_miniprogram_order,model_pos_miniprogram_order,point_of_sale.group_pos_user,1,1,1,1 +access_pos_miniprogram_order_line,access_pos_miniprogram_order_line,model_pos_miniprogram_order_line,point_of_sale.group_pos_user,1,1,1,1 +access_pos_miniprogram_order,access_pos_miniprogram_order,model_pos_miniprogram_order,wechat_miniprogram.group_miniprogram_user,1,0,0,0 +access_pos_miniprogram_order_line,access_pos_miniprogram_order_line,model_pos_miniprogram_order_line,wechat_miniprogram.group_miniprogram_user,1,0,0,0 +access_pos_multi_session,access_pos_multi_session,model_pos_multi_session,wechat_miniprogram.group_miniprogram_user,1,0,0,0 +access_res_partner,access_res_partner,base.model_res_partner,wechat_miniprogram.group_miniprogram_user,1,0,0,0 diff --git a/pos_wechat_miniprogram/static/src/css/pos_backend_style.css b/pos_wechat_miniprogram/static/src/css/pos_backend_style.css new file mode 100644 index 000000000..255466bcb --- /dev/null +++ b/pos_wechat_miniprogram/static/src/css/pos_backend_style.css @@ -0,0 +1,7 @@ +.o_form_view .oe_miniprogram_banner > img { + max-height: 150px; + max-width: 320px; + margin-bottom: 10px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); + border: none; +} diff --git a/pos_wechat_miniprogram/static/src/css/pos_frontend_style.css b/pos_wechat_miniprogram/static/src/css/pos_frontend_style.css new file mode 100644 index 000000000..7b3f9d521 --- /dev/null +++ b/pos_wechat_miniprogram/static/src/css/pos_frontend_style.css @@ -0,0 +1,92 @@ +/* Order Selector widget*/ +.pos .pos-rightheader .paid .deleteorder-button { + background: #a7a7a7!important; + pointer-events: none; + color: #393939; + box-shadow: initial; +} +/* Product Screen */ +.pos .product-screen.paid .leftpane .numpad { + display: none!important; +} +.pos .product-screen.paid .leftpane .control-buttons { + padding: 8px 16px 0px 10px; +} +.pos .product-screen.paid .leftpane .control-buttons .control-button { + display: none; +} +.pos .product-screen .leftpane .control-buttons .control-button { + transition: initial; +} +.pos .product-screen.paid .leftpane .control-buttons .order-submit { + display: inline-block; + margin-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + height: 52px; + font-weight: bold; + vertical-align: middle; + line-height: 52px; +} +.pos .product-screen.paid .leftpane .actionpad { + display: flex; + width: initial; + float: initial; + flex-flow: row wrap; + margin-right: 16px; + margin-top: 5px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.pos .product-screen.paid .leftpane .actionpad .pay { + display: none; +} +.pos .disable-products { + background: #00000036!important; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 2; +} +.pos .product-screen.paid .rightpane .layout-table { + z-index: 1; + pointer-events: none; +} +.pos .product-screen.paid .actionpad .button.validate { + height: 162px; + font-size: 26px; +} +.pos .product-screen.paid .actionpad .button.validate .pay-circle { + display: block; + font-size: 32px; + line-height: 54px; + padding-top: 6px; + background: rgb(86, 86, 86); + color: white; + width: 60px; + margin: auto; + border-radius: 30px; + margin-bottom: 10px; +} +.pos .product-screen.paid .actionpad .button.validate .pay-circle .fa { + position: relative; + top: -2px; +} +.pos .product-screen.paid .set-customer { + font-size: 18px; +} +/* Client Screen */ +.pos .clientlist-screen.paid .client-list { + display: none; +} +.pos .clientlist-screen.paid .top-content span { + display: none; +} +.pos .clientlist-screen.paid .top-content .back { + display: initial; +} +.pos .clientlist-screen.paid .edit-buttons { + display: none; +} diff --git a/pos_wechat_miniprogram/static/src/js/chrome.js b/pos_wechat_miniprogram/static/src/js/chrome.js new file mode 100644 index 000000000..53fff81aa --- /dev/null +++ b/pos_wechat_miniprogram/static/src/js/chrome.js @@ -0,0 +1,21 @@ +/* Copyright 2018 Dinar Gabbasov + License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */ +odoo.define('pos_wechat_miniprogram.chrome', function(require){ + "use strict"; + + var chrome = require('point_of_sale.chrome'); + + chrome.OrderSelectorWidget.include({ + renderElement: function() { + this._super(); + var order = this.pos.get_order(); + if (order && order.miniprogram_order && order.miniprogram_order.state === "done") { + $(this.el).addClass('paid'); + } else { + $(this.el).removeClass('paid'); + } + } + }); + + return chrome; +}); diff --git a/pos_wechat_miniprogram/static/src/js/models.js b/pos_wechat_miniprogram/static/src/js/models.js new file mode 100644 index 000000000..a78a961c9 --- /dev/null +++ b/pos_wechat_miniprogram/static/src/js/models.js @@ -0,0 +1,302 @@ +/* Copyright 2018 Dinar Gabbasov + License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */ +odoo.define('pos_wechat_miniprogram.models', function(require){ + "use strict"; + + var models = require('point_of_sale.models'); + var rpc = require('web.rpc'); + + models.load_fields('account.journal', ['wechat']); + + models.load_models({ + model: 'pos.miniprogram.order', + fields: [], + domain: function(self) { + return [['confirmed_from_pos', '=', false], ['shop_id', '=', self.config.shop_id[0]]]; + }, + loaded: function(self, orders) { + if (self.config.allow_message_from_miniprogram) { + // load not confirmed orders + orders.forEach(function (order) { + self.unconfirmed_miniprogram_orders_ids.push(order.id); + order.lines_ids = []; + self.get_miniprogram_order_lines_by_order_id(order.id).then(function (lines) { + if (Array.isArray(lines)) { + order.lines_ids = order.lines_ids.concat(lines); + } else { + order.lines_ids.push(lines); + } + self.on_wechat_miniprogram(order); + }); + }); + } + }, + }); + + var PosModelSuper = models.PosModel; + models.PosModel = models.PosModel.extend({ + initialize: function() { + var self = this; + this.unconfirmed_miniprogram_orders_ids = []; + PosModelSuper.prototype.initialize.apply(this, arguments); + this.bus.add_channel_callback("wechat.miniprogram", this.on_wechat_miniprogram, this); + this.ready.then(function() { + + var mp_orders = self.get('orders').filter(function(order) { + if (order.miniprogram_order && order.miniprogram_order.id) { + return order.miniprogram_order; + } + }); + + var not_found = mp_orders.map(function(r) { + return r.miniprogram_order.id; + }); + + self.unconfirmed_miniprogram_orders_ids.forEach(function(id) { + not_found = _.without(not_found, id); + }); + + _.each(not_found, function(id) { + var order = self.get('orders').find(function(r){ + return r.miniprogram_order && id === r.miniprogram_order.id; + }); + order.destroy({'reason':'abandon'}); + }); + }); + }, + get_miniprogram_order_lines_by_order_id: function (id) { + return rpc.query({ + model: 'pos.miniprogram.order.line', + method: 'search_read', + args: [[['order_id', '=', id]], []] + }); + }, + on_wechat_miniprogram: function(message) { + var order = this.get('orders').find(function(item) { + if (item.miniprogram_order) { + return item.miniprogram_order.id === message.id; + } + }); + if (order) { + this.update_miniprogram_order(order, message); + } else { + this.create_miniprogram_order(message); + } + }, + update_miniprogram_order: function(order, data) { + var self = this; + var not_found = order.orderlines.map(function(r) { + return r.miniprogram_line.id; + }); + + data.lines_ids.forEach(function(l) { + var line = order.orderlines.find(function(r){ + // search by mini-program orderline id + return l.id === r.miniprogram_line.id; + }); + + not_found = _.without(not_found, l.id); + + if (line) { + // update line + line.apply_updates_miniprogram_line(l); + } else { + // create new line and add to the Order + line = self.create_orderline_by_miniprogram_data(order, l); + if (line) { + order.orderlines.add(line); + } + } + }); + + // remove old lines + _.each(not_found, function(id){ + var line = order.orderlines.find(function(r){ + return id === r.miniprogram_line.id; + }); + order.orderlines.remove(line); + }); + + // update exist order + order.apply_updates_miniprogram_order(data); + }, + create_miniprogram_order: function(data) { + var self = this; + // get current order + var current_order = this.get_order(); + // create new order + var order = new models.Order({}, {mp_data: data, pos: this}); + // get and set partner + if(typeof data.partner_id === 'undefined') { + order.set_client(null); + } else { + var client = order.pos.db.get_partner_by_id(data.partner_id[0]); + if(!client) { + $.when(this.load_new_partners_by_id(data.partner_id[0])).then(function(new_client){ + new_client = order.pos.db.get_partner_by_id(data.partner_id); + order.set_client(new_client); + }); + } + order.set_client(client); + } + + this.get('orders').add(order); + // set current order + this.set('selectedOrder', current_order); + // create orderlines + data.lines_ids.forEach(function(l) { + var line = self.create_orderline_by_miniprogram_data(order, l); + if (line) { + order.orderlines.add(line); + } + }); + + // update floor screen + var floor_screen = this.gui.screen_instances.floors; + if (floor_screen && this.gui.get_current_screen() === "floors") { + floor_screen.renderElement(); + } + + // auto print payed orders + if(this.printers.length && order.hasChangesToPrint() && this.config.auto_print_miniprogram_orders && order.miniprogram_order && order.miniprogram_order.state === "done"){ + order.printChanges(); + order.saveChanges(); + } + }, + create_orderline_by_miniprogram_data: function(order, data) { + var product = this.db.get_product_by_id(data.product_id[0]); + if (product) { + var line = new models.Orderline({}, {pos: this, order: order, product: product, mp_data: data}); + if (typeof data.quantity !== 'undefined' && data.quantity !== line.quantity){ + line.set_quantity(data.quantity); + } + if (typeof data.price !== 'undefined' && data.price !== line.price) { + line.set_unit_price(data.price); + } + return line; + } + return false; + }, + load_new_partners_by_id: function(partner_id){ + var self = this; + var def = new $.Deferred(); + var fields = _.find(this.models,function(model){ + return model.model === 'res.partner'; + }).fields; + + var domain = [['id','=',partner_id]]; + rpc.query({ + model: 'res.partner', + method: 'search_read', + args: [domain, fields], + }, { + timeout: 3000, + shadow: true, + }).then(function(partners){ + // check if the partners we got were real updates + if (self.db.add_partners(partners)) { + def.resolve(); + } else { + def.reject(); + } + }, function(type,err){ + if (err) { + console.log(err); + } + def.reject(); + }); + return def; + }, + get_mp_cashregister: function() { + return this.cashregisters.find(function(c) { + return c.journal.wechat && c.journal.wechat === 'jsapi'; + }); + }, + ms_create_order: function(options) { + var data = options.data; + var json = options.json; + if (data && data.miniprogram_order && data.miniprogram_order.id) { + return false; + } + if (json && json.miniprogram_order && json.miniprogram_order.id) { + return false; + } + return PosModelSuper.prototype.ms_create_order.apply(this, arguments); + } + }); + + var OrderSuper = models.Order; + models.Order = models.Order.extend({ + initialize: function (attributes, options) { + options = options || {}; + this.miniprogram_order = {}; + OrderSuper.prototype.initialize.apply(this, arguments); + if (options.mp_data) { + this.apply_updates_miniprogram_order(options.mp_data); + } + }, + apply_updates_miniprogram_order: function(data) { + // all mini-program data + this.miniprogram_order = data; + + // common data for the order and for the order of mini-program + this.table = this.pos.tables_by_id[data.table_id[0]]; + this.floor = this.pos.floors_by_id[data.floor_id[0]] || null; + this.customer_count = data.customer_count || 1; + this.note = data.note; + this.to_invoice = data.to_invoice; + + // save to db + this.trigger('change', this); + }, + export_as_JSON: function() { + var data = OrderSuper.prototype.export_as_JSON.apply(this, arguments); + data.miniprogram_order = this.miniprogram_order; + data.miniprogram_order_ref = this.uid; + return data; + }, + init_from_JSON: function(json) { + this.miniprogram_order = json.miniprogram_order; + OrderSuper.prototype.init_from_JSON.call(this, json); + }, + }); + + var OrderlineSuper = models.Orderline; + models.Orderline = models.Orderline.extend({ + initialize: function(attr,options) { + options = options || {}; + this.miniprogram_line = {}; + OrderlineSuper.prototype.initialize.apply(this,arguments); + if (options.mp_data) { + this.apply_updates_miniprogram_line(options.mp_data); + } + }, + apply_updates_miniprogram_line: function(data) { + // all mini-program data + this.miniprogram_line = data; + + // common data for the orderline and for the line of mini-program + if (this.quantity !== data.quantity){ + this.set_quantity(data.quantity); + } + if (this.price !== data.price) { + this.set_unit_price(data.price); + } + if (data.note) { + this.set_note(data.note); + } + // save to db + this.trigger('change',this); + this.order.trigger('change',this); + }, + export_as_JSON: function() { + var data = OrderlineSuper.prototype.export_as_JSON.apply(this, arguments); + data.miniprogram_line = this.miniprogram_line; + return data; + }, + init_from_JSON: function(json) { + this.miniprogram_line = json.miniprogram_line; + OrderlineSuper.prototype.init_from_JSON.call(this, json); + } + }); +}); diff --git a/pos_wechat_miniprogram/static/src/js/screens.js b/pos_wechat_miniprogram/static/src/js/screens.js new file mode 100644 index 000000000..50a73b8ed --- /dev/null +++ b/pos_wechat_miniprogram/static/src/js/screens.js @@ -0,0 +1,155 @@ +/* Copyright 2018 Dinar Gabbasov + License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */ +odoo.define('pos_wechat_miniprogram.screens', function(require){ + "use strict"; + + var screens = require('point_of_sale.screens'); + var core = require('web.core'); + var _t = core._t; + + + screens.ProductScreenWidget.include({ + show: function(reset) { + this._super(); + var self = this; + if (reset) { + var order = this.pos.get_order(); + if (order && order.miniprogram_order && order.miniprogram_order.state === "done") { + $(this.el).addClass('paid'); + $(this.el).find('.rightpane .layout-table').before('
'); + var validate =""; + $(this.el).find('.actionpad').append(validate); + } else { + $(this.el).removeClass('paid'); + $(this.el).find('.rightpane .disable-products').remove(); + $(this.el).find('.actionpad .validate').remove(); + } + this.$('.validate').click(function(){ + self.actionpad.click_validate_paid_order(); + }); + } + } + }); + + screens.PaymentScreenWidget.include({ + render_paymentmethods: function() { + var methods = this._super(); + var jsapi_journal = this.pos.get_mp_cashregister(); + if (jsapi_journal) { + var el = methods.find("[data-id=" + jsapi_journal.journal_id[0] + "]"); + if (el.length) { + el.remove(); + } + } + return methods; + }, + }); + + screens.ClientListScreenWidget.include({ + show: function(){ + this._super(); + var order = this.pos.get_order(); + if (order && order.miniprogram_order && order.miniprogram_order.state === "done") { + $(this.el).addClass('paid'); + } else { + $(this.el).removeClass('paid'); + } + } + }); + + screens.ActionpadWidget.include({ + click_validate_paid_order: function() { + var order = this.pos.get_order(); + var message = { + 'title': _t('Order Validate') + }; + if(order.hasChangesToPrint()) { + message.body = _t('You have not printed products in the kitchen. Validate without print?'); + } else { + message.body = _t('Confirm the paid order?'); + } + this.show_validate_popup(message); + }, + show_validate_popup: function(message) { + var self = this; + this.pos.gui.show_popup('confirm', { + 'title': message.title, + 'body': message.body, + confirm: function() { + self.validate_paid_order(); + }, + }); + }, + validate_paid_order: function() { + var order = this.pos.get_order(); + var cashregister = this.pos.get_mp_cashregister(); + order.add_paymentline(cashregister); + var amount = order.get_total_with_tax(); + order.selected_paymentline.set_amount(amount); + if (order.is_paid()) { + this.finalize_validation(); + } + }, + finalize_validation: function() { + var self = this; + var order = this.pos.get_order(); + order.initialize_validation_date(); + order.finalized = true; + + if (order.is_to_invoice()) { + var invoiced = this.pos.push_and_invoice_order(order); + this.invoicing = true; + + invoiced.fail(function(error){ + self.invoicing = false; + order.finalized = false; + if (error.message === 'Missing Customer') { + self.gui.show_popup('confirm',{ + 'title': _t('Please select the Customer'), + 'body': _t('You need to select the customer before you can invoice an order.'), + confirm: function(){ + self.gui.show_screen('clientlist'); + }, + }); + } else if (error.code < 0) { + // XmlHttpRequest Errors + self.gui.show_popup('error',{ + 'title': _t('The order could not be sent'), + 'body': _t('Check your internet connection and try again.'), + }); + } else if (error.code === 200) { + // OpenERP Server Errors + self.gui.show_popup('error-traceback',{ + 'title': error.data.message || _t("Server Error"), + 'body': error.data.debug || _t('The server encountered an error while receiving your order.'), + }); + } else { + // other Error + self.gui.show_popup('error',{ + 'title': _t("Unknown Error"), + 'body': _t("The order could not be sent to the server due to an unknown error"), + }); + } + }); + invoiced.done(function(){ + self.invoicing = false; + self.gui.show_screen('receipt'); + }); + } else { + this.pos.push_order(order); + this.gui.show_screen('receipt'); + } + + }, + }); + + screens.OrderWidget.include({ + rerender_orderline: function (order_line) { + if (order_line.node && order_line.node.parentNode) { + return this._super(order_line); + } + } + }); + + return screens; +}); diff --git a/pos_wechat_miniprogram/tests/__init__.py b/pos_wechat_miniprogram/tests/__init__.py new file mode 100644 index 000000000..453af8dd1 --- /dev/null +++ b/pos_wechat_miniprogram/tests/__init__.py @@ -0,0 +1,2 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from . import test_pos_miniprogram diff --git a/pos_wechat_miniprogram/tests/test_pos_miniprogram.py b/pos_wechat_miniprogram/tests/test_pos_miniprogram.py new file mode 100644 index 000000000..64313bd44 --- /dev/null +++ b/pos_wechat_miniprogram/tests/test_pos_miniprogram.py @@ -0,0 +1,189 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +import logging + +try: + from unittest.mock import patch +except ImportError: + from mock import patch + +from odoo.tests.common import HttpCase +from odoo import api + + +_logger = logging.getLogger(__name__) + + +class TestQCloudSMS(HttpCase): + at_install = True + post_install = True + + def setUp(self): + super(TestQCloudSMS, self).setUp() + self.phantom_env = api.Environment(self.registry.test_cr, self.uid, {}) + self.user = self.phantom_env.user + self.partner = self.user.partner_id + + self.partner.write({ + 'mobile': '+1234567890' + }) + + self.Order = self.phantom_env['pos.miniprogram.order'] + + self.product1 = self.phantom_env['product.product'].create({ + 'name': 'Product1', + }) + self.product2 = self.phantom_env['product.product'].create({ + 'name': 'Product2', + }) + + self.lines = [ + { + "product_id": self.product1.id, + "name": "Product 1 Name", + "quantity": 1, + "price": 1, + "category": "123456", + "description": "翻译服务器错误", + }, + { + "product_id": self.product2.id, + "name": "Product 2 Name", + "quantity": 1, + "price": 2, + "category": "123456", + "description": "網路白目哈哈", + } + ] + + self.create_vals = { + 'name': 'Test Order', + 'note': 'This is test Order note', + 'table_id': 1, + 'floor_id': 1, + 'customer_count': 4, + 'packingMethods': 'indoors', + 'to_invoice': True, + } + + # add patch + patcher_possible_number = patch('phonenumbers.is_possible_number', wraps=lambda *args: True) + patcher_possible_number.start() + self.addCleanup(patcher_possible_number.stop) + + patcher_valid_number = patch('phonenumbers.is_valid_number', wraps=lambda *args: True) + patcher_valid_number.start() + self.addCleanup(patcher_valid_number.stop) + + patcher = patch('wechatpy.WeChatPay.check_signature', wraps=lambda *args: True) + patcher.start() + self.addCleanup(patcher.stop) + + def _check_verification_code(self): + code = self.user.verification_code + return self.user.check_verification_code(code) + + def _patch_post_requests(self, response_json, patch_url): + + def api_request(url=None, req=None, httpclient=None): + _logger.debug("Request data: req - %s, httpclient - %s", req, httpclient) + return response_json + + patcher = patch(patch_url, wraps=api_request) + patcher.start() + self.addCleanup(patcher.stop) + + def _sms_template_mobile_number_verification(self, mobile): + response_json = { + "result": 0, + "errmsg": "OK", + "ext": "", + "fee": 1, + "sid": "xxxxxxx" + } + patch_url = 'qcloudsms_py.util.api_request' + self._patch_post_requests(response_json, patch_url) + + return self.user.template_sms_mobile_number_verification(mobile) + + def _sms_mobile_number_verification(self, mobile): + response_json = { + "result": 0, + "errmsg": "OK", + "ext": "", + "fee": 1, + "sid": "xxxxxxx" + } + patch_url = 'qcloudsms_py.util.api_request' + self._patch_post_requests(response_json, patch_url) + + return self.user.sms_mobile_number_verification(mobile) + + def _create_from_miniprogram_ui(self, create_vals, lines): + post_result = { + 'pay/unifiedorder': { + 'trade_type': 'JSAPI', + 'result_code': 'SUCCESS', + 'prepay_id': 'qweqweqwesadsd2113', + 'nonce_str': 'wsdasd12312eaqsd21q3' + } + } + + def post(url, data): + _logger.debug("Request data for %s: %s", url, data) + return post_result[url] + + # patch wechat + patcher = patch('wechatpy.pay.base.BaseWeChatPayAPI._post', wraps=post) + patcher.start() + self.addCleanup(patcher.stop) + + return self.phantom_env['pos.miniprogram.order'].create_from_miniprogram_ui(lines, create_vals) + + def test_sms_mobile_number_verification(self): + mobile = '+1234567890' + response = self._sms_mobile_number_verification(mobile) + self.assertEquals(response.get('result'), 0, 'Could not send message') + res = self._check_verification_code() + self.assertTrue(res.get('result'), res.get('message')) + + def test_template_sms_mobile_number_verification(self): + mobile = '+1234567890' + + template = self.phantom_env['qcloud.sms.template'].create({ + 'name': 'Verification by sms template', + 'domestic_sms_template_ID': '123', + 'domestic_sms_sign': 'Test', + 'international_sms_template_ID': '321', + 'international_sms_sign': 'Test' + }) + self.phantom_env['ir.config_parameter'].sudo().set_param('qcloud.sms_template', template.id) + response = self._sms_template_mobile_number_verification(mobile) + self.assertEquals(response.get('result'), 0, 'Could not send message') + res = self._check_verification_code() + self.assertTrue(res.get('result'), res.get('message')) + + def test_create_and_pay_from_miniprogram_ui(self): + """ + Create order from mini-program UI, pay, and send the Order to POS + """ + self.user.write({ + 'number_verified': True + }) + # Pay method ('now' - Pay from mini-program, 'later' - Pay from POS) + self.create_vals['pay_method'] = 'now' + res = self._create_from_miniprogram_ui(create_vals=self.create_vals, lines=self.lines) + order = self.Order.search([('wechat_order_id', '=', res.get('order_id'))]) + self.assertEqual(order.state, 'draft', 'Just created order has wrong state. ') + + def test_create_without_pay_from_miniprogram_ui(self): + """ + Create order from mini-program UI and send the Order to POS + """ + self.user.write({ + 'number_verified': True + }) + # Pay method ('now' - Pay from mini-program, 'later' - Pay from POS) + self.create_vals['pay_method'] = 'later' + order = self._create_from_miniprogram_ui(create_vals=self.create_vals, lines=self.lines) + self.assertEqual(order.state, 'draft', 'Just created order has wrong state. ') diff --git a/pos_wechat_miniprogram/views/pos_config_view.xml b/pos_wechat_miniprogram/views/pos_config_view.xml new file mode 100644 index 000000000..d0a9fb10f --- /dev/null +++ b/pos_wechat_miniprogram/views/pos_config_view.xml @@ -0,0 +1,33 @@ + + + + + pos.config.form.view.inherit + pos.config + + + +

WeChat mini-program

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
diff --git a/pos_wechat_miniprogram/views/pos_multi_session_restaurant_view.xml b/pos_wechat_miniprogram/views/pos_multi_session_restaurant_view.xml new file mode 100644 index 000000000..6c909abe5 --- /dev/null +++ b/pos_wechat_miniprogram/views/pos_multi_session_restaurant_view.xml @@ -0,0 +1,39 @@ + + + + + + pos.config.form.view.inherit + pos.config + + + +
+
+
+
+
+
+
+ + + pos.multi_session.form + pos.multi_session + + + + + + + + + + +
diff --git a/pos_wechat_miniprogram/views/pos_restaurant_view.xml b/pos_wechat_miniprogram/views/pos_restaurant_view.xml new file mode 100644 index 000000000..51b1b9d08 --- /dev/null +++ b/pos_wechat_miniprogram/views/pos_restaurant_view.xml @@ -0,0 +1,15 @@ + + + + + Restaurant Table + restaurant.table + + + + + + + + diff --git a/pos_wechat_miniprogram/views/pos_wechat_miniprogram_view.xml b/pos_wechat_miniprogram/views/pos_wechat_miniprogram_view.xml new file mode 100644 index 000000000..ee7ef3e3a --- /dev/null +++ b/pos_wechat_miniprogram/views/pos_wechat_miniprogram_view.xml @@ -0,0 +1,115 @@ + + + + + pos.miniprogram.order.form + pos.miniprogram.order + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pos.miniprogram.order.tree + pos.miniprogram.order + + + + + + + + + + + + + + + + Orders + ir.actions.act_window + pos.miniprogram.order + form + tree,form + + [] + +

+ Use a WeChat mini-program to create a new order. +

+
+
+ + + + + diff --git a/pos_wechat_miniprogram/views/product_view.xml b/pos_wechat_miniprogram/views/product_view.xml new file mode 100644 index 000000000..5c9bba7fc --- /dev/null +++ b/pos_wechat_miniprogram/views/product_view.xml @@ -0,0 +1,17 @@ + + + + + product.template.form.inherit + product.template + + + + + + + + + + diff --git a/pos_wechat_miniprogram/views/res_config_settings_view.xml b/pos_wechat_miniprogram/views/res_config_settings_view.xml new file mode 100644 index 000000000..3ed616f91 --- /dev/null +++ b/pos_wechat_miniprogram/views/res_config_settings_view.xml @@ -0,0 +1,28 @@ + + + + + res.config.settings.view.form.inherit.base.setup + res.config.settings + + + +
+
+
+
+
+
+
+
diff --git a/pos_wechat_miniprogram/views/template.xml b/pos_wechat_miniprogram/views/template.xml new file mode 100644 index 000000000..493929350 --- /dev/null +++ b/pos_wechat_miniprogram/views/template.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/pos_wechat_miniprogram/wizard/__init__.py b/pos_wechat_miniprogram/wizard/__init__.py new file mode 100644 index 000000000..c2f2e65ac --- /dev/null +++ b/pos_wechat_miniprogram/wizard/__init__.py @@ -0,0 +1,3 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from . import qrcode +from . import pos_payment diff --git a/pos_wechat_miniprogram/wizard/pos_payment.py b/pos_wechat_miniprogram/wizard/pos_payment.py new file mode 100644 index 000000000..00bd19c70 --- /dev/null +++ b/pos_wechat_miniprogram/wizard/pos_payment.py @@ -0,0 +1,19 @@ +# Copyright 2018 Ivan Yelizariev +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import models, fields, api + + +class PosMakePayment(models.TransientModel): + _inherit = 'pos.make.payment' + + journal_wechat = fields.Selection(related='journal_id.wechat') + wechat_order_id = fields.Many2one('wechat.order', string='WeChat Order to refund') + micropay_id = fields.Many2one('wechat.micropay', string='Micropay to refund') + + @api.onchange('order_ref', 'journal_wechat') + def update_wechat_order(self): + super(PosMakePayment, self).update_wechat_order() + if self.journal_wechat == 'jsapi': + record = self.env['pos.miniprogram.order'].search([('order_ref', '=', self.order_ref)]).wechat_order_id + self.wechat_order_id = record + self.micropay_id = False diff --git a/pos_wechat_miniprogram/wizard/pos_payment_views.xml b/pos_wechat_miniprogram/wizard/pos_payment_views.xml new file mode 100644 index 000000000..94cb957a8 --- /dev/null +++ b/pos_wechat_miniprogram/wizard/pos_payment_views.xml @@ -0,0 +1,15 @@ + + + + + pos.make.payment.form + pos.make.payment + + + + {'invisible': [('journal_wechat', 'not in', ['native', 'jsapi'])]} + + + + diff --git a/pos_wechat_miniprogram/wizard/qrcode.py b/pos_wechat_miniprogram/wizard/qrcode.py new file mode 100644 index 000000000..b836c50e4 --- /dev/null +++ b/pos_wechat_miniprogram/wizard/qrcode.py @@ -0,0 +1,40 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import api, fields, models +import logging + +_logger = logging.getLogger(__name__) + + +class PosTableQrCode(models.TransientModel): + _name = 'pos.table.qrcode.wizard' + _description = 'Open QR code window of tables' + + floor_id = fields.Many2one('restaurant.floor', 'Floor', required=True) + table_ids = fields.Many2many('restaurant.table', string='Tables', domain="[('floor_id', '=', floor_id)]") + quantity = fields.Integer(string='Quantity', help='The quantity of QR Code for each table', default=1) + + @api.onchange('floor_id') + def _on_change_floor(self): + if self.floor_id: + self.table_ids = self.env['restaurant.table'].search([('floor_id', '=', self.floor_id.id)]) + else: + self.table_ids = None + + @api.multi + def generate_qrcode(self): + """ + Call when button 'Get QR Code' clicked. + """ + data = { + 'ids': self.ids, + 'model': self._name, + 'form': { + 'name': self.floor_id.name, + 'table_ids': [table.id for table in self.table_ids], + 'quantity': self.quantity + }, + } + # use `module_name.report_id` as reference. + # `report_action()` will call `get_report_values()` and pass `data` automatically. + return self.env.ref('pos_wechat_miniprogram.generate_qr_code_report').report_action(self, data=data) diff --git a/pos_wechat_miniprogram/wizard/qrcode.xml b/pos_wechat_miniprogram/wizard/qrcode.xml new file mode 100644 index 000000000..f10cbde2d --- /dev/null +++ b/pos_wechat_miniprogram/wizard/qrcode.xml @@ -0,0 +1,34 @@ + + + + + pos.table.qrcode.wizard.form + pos.table.qrcode.wizard + +
+ + + + + + + +
+
+
+
+
+ + + QR Code + pos.table.qrcode.wizard + form + new + + + + +
diff --git a/qcloud_sms/models/qcloud_sms.py b/qcloud_sms/models/qcloud_sms.py index d1476ab25..b5290b8b6 100644 --- a/qcloud_sms/models/qcloud_sms.py +++ b/qcloud_sms/models/qcloud_sms.py @@ -19,6 +19,7 @@ class QCloudSMS(models.Model): _name = 'qcloud.sms' _description = 'SMS Messages' + _rec_name = 'send_datetime' _order = 'id desc' STATE_SELECTION = [ @@ -86,7 +87,7 @@ def _send_message(self, message, partner_id, sms_type, **kwargs): partner = self.env['res.partner'].browse(partner_id) vals = { 'message': message, - 'partner_ids': partner, + 'partner_ids': [(4, partner.id)], 'sms_type': sms_type, } @@ -149,7 +150,7 @@ def _send_group_message(self, message, partner_ids, sms_type, **kwargs): vals = { 'message': message, - 'partner_ids': partners, + 'partner_ids': [(4, p.id) for p in partners], 'sms_type': sms_type, } @@ -200,7 +201,7 @@ class QCloudSMSTemplate(models.Model): name = fields.Char(string='Name', help='The template name') - domestic_sms_template_ID = fields.Integer( + domestic_sms_template_ID = fields.Char( string='Domestic SMS Template ID', help='SMS Template ID is the Tencent Cloud SMS template (the specific content of the SMS message to be sent).' ) @@ -213,7 +214,7 @@ class QCloudSMSTemplate(models.Model): help='SMS Signature is the Tencent Cloud SMS signature (an identifier added before the message body for ' 'identification of the company or business.).' ) - international_sms_template_ID = fields.Integer( + international_sms_template_ID = fields.Char( string='International SMS Template ID', help='SMS Template ID is the Tencent Cloud SMS template (the specific content of the SMS message to be sent).' ) @@ -222,25 +223,28 @@ class QCloudSMSTemplate(models.Model): help="Parameters must be separated by commas. If the template has no parameters, leave it empty." ) international_sms_sign = fields.Char( - string='International SMS Signature ID', + string='International SMS Signature', help='SMS Signature is the Tencent Cloud SMS signature (an identifier added before the message body for ' 'identification of the company or business.).' ) @api.multi - def _get_sms_params_by_country_code(self, code): + def _get_sms_params_by_country_code(self, code, **kwargs): self.ensure_one() res = {} # China Country Code 86 (use domestics templates) if code == '86': res['template_ID'] = self.domestic_sms_template_ID or '' - params = self.domestic_template_params or '' + params = self.domestic_template_params or False res['sign'] = self.domestic_sms_sign or '' else: res['template_ID'] = self.international_sms_template_ID or '' - params = self.international_template_params or '' + params = self.international_template_params or False res['sign'] = self.international_sms_sign or '' + if params is False: + params = kwargs.get('params', '') + res['template_params'] = params.split(',') return res @@ -271,7 +275,7 @@ def _send_template_message(self, partner_id, **kwargs): partner = self.env['res.partner'].browse(partner_id) vals = { - 'partner_ids': partner, + 'partner_ids': [(4, partner.id)], 'template_id': self.id, } @@ -297,7 +301,7 @@ def _send_template_message(self, partner_id, **kwargs): _logger.debug("Country code: %s, Mobile number: %s", country_code, national_number) - params = self._get_sms_params_by_country_code(country_code) + params = self._get_sms_params_by_country_code(country_code, **kwargs) _logger.debug("SMS params: %s", params) @@ -343,7 +347,7 @@ def _send_template_group_message(self, partner_ids, **kwargs): partners = self.env['res.partner'].browse(partner_ids) vals = { - 'partner_ids': partners, + 'partner_ids': [(4, p.id) for p in partners], 'template_id': self.id, } @@ -375,7 +379,7 @@ def _send_template_group_message(self, partner_ids, **kwargs): _logger.debug("Country code: %s, Mobile numbers: %s", country_code, national_number_list) - params = self._get_sms_params_by_country_code(country_code) + params = self._get_sms_params_by_country_code(country_code, **kwargs) _logger.debug("SMS params: %s", params) diff --git a/qcloud_sms/views/res_config.xml b/qcloud_sms/views/res_config.xml index fa80b3986..fb377436d 100644 --- a/qcloud_sms/views/res_config.xml +++ b/qcloud_sms/views/res_config.xml @@ -4,7 +4,7 @@ qcloud.sms.tree qcloud.sms - + @@ -16,7 +16,7 @@ qcloud.sms.form qcloud.sms -
+
diff --git a/wechat_miniprogram/controllers/wechat_controllers.py b/wechat_miniprogram/controllers/wechat_controllers.py index ec5e9baf4..2a203fe6a 100644 --- a/wechat_miniprogram/controllers/wechat_controllers.py +++ b/wechat_miniprogram/controllers/wechat_controllers.py @@ -14,7 +14,7 @@ class WechatMiniProgramController(http.Controller): @http.route('/wechat/miniprogram/authenticate', type='json', auth='public', csrf=False) - def authenticate(self, code, user_info, test_cr=False): + def authenticate(self, code, user_info=False, test_cr=False): """ :param code: After the user is permitted to log in on the WeChat mini-program, the callback content will bring the code (five-minute validity period). The developer needs to send the code to the backend @@ -28,7 +28,8 @@ def authenticate(self, code, user_info, test_cr=False): _logger.debug('Authenticate on WeChat server: openid - %s, session_key - %s', openid, session_key) if not openid or not session_key: - raise UserError(_('Unable to get data from WeChat server : openid - %s, session_key - %s') % (openid, session_key)) + raise UserError( + _('Unable to get data from WeChat server : openid - %s, session_key - %s') % (openid, session_key)) User = request.env['res.users'].sudo() user = User.search([('openid', '=', openid)]) @@ -37,6 +38,10 @@ def authenticate(self, code, user_info, test_cr=False): 'wechat_session_key': session_key, }) else: + if not user_info: + raise UserError( + _('Unable to get user info from WeChat mini-program: %s') % user_info) + country = request.env['res.country'].search([('name', 'like', '%'+user_info.get('country')+'%')], limit=1) name = user_info.get('nickName') login = "wechat_%s" % openid diff --git a/wechat_miniprogram/models/__init__.py b/wechat_miniprogram/models/__init__.py index 4f4ec4bc2..aa38ac4ed 100644 --- a/wechat_miniprogram/models/__init__.py +++ b/wechat_miniprogram/models/__init__.py @@ -2,3 +2,4 @@ from . import wechat_order from . import ir_config_parameter from . import res_users +from . import account_journal diff --git a/wechat_miniprogram/models/account_journal.py b/wechat_miniprogram/models/account_journal.py new file mode 100644 index 000000000..4049b7c06 --- /dev/null +++ b/wechat_miniprogram/models/account_journal.py @@ -0,0 +1,9 @@ +# Copyright 2018 Dinar Gabbasov +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from odoo import models, fields + + +class Journal(models.Model): + _inherit = 'account.journal' + + wechat = fields.Selection(selection_add=[('jsapi', 'Pay from WeChat mini-program')]) diff --git a/wechat_miniprogram/models/ir_config_parameter.py b/wechat_miniprogram/models/ir_config_parameter.py index 4d0d4081b..0ecc5e50c 100644 --- a/wechat_miniprogram/models/ir_config_parameter.py +++ b/wechat_miniprogram/models/ir_config_parameter.py @@ -2,14 +2,19 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). import logging import werkzeug.urls +import base64 +import json +import requests +from wechatpy import WeChatPay -from odoo import models, api +from odoo import models, api, _ +from odoo.exceptions import UserError _logger = logging.getLogger(__name__) try: - from wechatpy import WeChatPay + from Crypto.Cipher import AES except ImportError as err: _logger.debug(err) @@ -50,3 +55,50 @@ def get_openid_url(self, code): url = '%s?%s' % (base_url, werkzeug.urls.url_encode(param)) _logger.debug('openid url: %s', url) return url + + def decrypt_wechat_miniprogram_data(self, session_key, encryptedData, iv): + # base64 decode + _logger.debug('Decrypt arguments: %s, %s', encryptedData, iv) + + sessionKey = base64.b64decode(session_key) + encryptedData = base64.b64decode(encryptedData) + iv = base64.b64decode(iv) + + cipher = AES.new(sessionKey, AES.MODE_CBC, iv) + data = self._unpad(cipher.decrypt(encryptedData)).decode('UTF-8') + decrypted = json.loads(data) + + _logger.debug('Decrypt result: %s', decrypted) + + if decrypted['watermark']['appid'] != self.sudo().get_param('wechat.miniprogram_app_id', ''): + raise UserError(_('Invalid Buffer')) + + return decrypted + + def _unpad(self, s): + return s[:-ord(s[len(s)-1:])] + + def get_miniprogram_access_token(self): + base_url = 'https://api.weixin.qq.com/cgi-bin/token' + param = { + 'appid': self.sudo().get_param('wechat.miniprogram_app_id', ''), + 'secret': self.sudo().get_param('wechat.miniprogram_app_secret', ''), + 'grant_type': 'client_credential' + } + url = '%s?%s' % (base_url, werkzeug.urls.url_encode(param)) + response = requests.get(url) + response.raise_for_status() + value = response.json() + _logger.debug('access_token value: %s', value) + access_token = value.get('access_token') + return access_token + + def get_qr_code(self, data, access_token=False): + base_url = 'https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode' + param = { + 'access_token': access_token or self.get_miniprogram_access_token(), + } + url = '%s?%s' % (base_url, werkzeug.urls.url_encode(param)) + response = requests.post(url, json=data) + img = base64.b64encode(response.content) + return img diff --git a/wechat_miniprogram/models/res_users.py b/wechat_miniprogram/models/res_users.py index a18c99873..8380a034e 100644 --- a/wechat_miniprogram/models/res_users.py +++ b/wechat_miniprogram/models/res_users.py @@ -11,9 +11,6 @@ class ResUsers(models.Model): _inherit = 'res.users' - openid = fields.Char('Openid') - wechat_session_key = fields.Char("WeChat session key") - @api.model def check_credentials(self, password): try: @@ -22,3 +19,10 @@ def check_credentials(self, password): res = self.sudo().search([('id', '=', self.env.uid), ('wechat_session_key', '=', password)]) if not res: raise + + +class ResPartner(models.Model): + _inherit = "res.partner" + + openid = fields.Char('Openid') + wechat_session_key = fields.Char("WeChat session key") diff --git a/wechat_miniprogram/models/wechat_order.py b/wechat_miniprogram/models/wechat_order.py index 188327baf..e29b54c00 100644 --- a/wechat_miniprogram/models/wechat_order.py +++ b/wechat_miniprogram/models/wechat_order.py @@ -25,6 +25,7 @@ def _create_jsapi_order(self, lines, create_vals): :returns order_id: Current order id result_json: Payments data for WeChat """ + debug = self.env['ir.config_parameter'].sudo().get_param('wechat.local_sandbox') == '1' vals = {