From 1969602a27a032f168e8943e1f9fbc053587f670 Mon Sep 17 00:00:00 2001 From: Mitchell Admin Date: Wed, 11 Aug 2021 11:09:29 +0000 Subject: [PATCH 1/3] :tada::one::three: wechat previous commits history: https://github.com/itpp-labs/pos-addons/commits/13.0/wechat > Made via .github/workflows/DINAR-PORT.yml --- wechat/Dockerfile | 10 + wechat/README.rst | 89 +++++ wechat/__init__.py | 3 + wechat/__manifest__.py | 29 ++ wechat/controllers/__init__.py | 2 + wechat/controllers/wechat_controllers.py | 37 ++ wechat/data/ir_sequence_data.xml | 58 +++ wechat/doc/changelog.rst | 9 + wechat/doc/index.rst | 73 ++++ wechat/i18n/wechat.pot | 428 +++++++++++++++++++++++ wechat/models/__init__.py | 6 + wechat/models/account_journal.py | 13 + wechat/models/ir_config_parameter.py | 44 +++ wechat/models/wechat_micropay.py | 108 ++++++ wechat/models/wechat_order.py | 290 +++++++++++++++ wechat/models/wechat_refund.py | 77 ++++ wechat/security/ir.model.access.csv | 5 + wechat/static/description/icon.png | Bin 0 -> 3035 bytes wechat/static/description/index.html | 111 ++++++ wechat/tests/__init__.py | 2 + wechat/tests/test_wechat_order.py | 157 +++++++++ wechat/views/account_journal_views.xml | 15 + wechat/views/account_menuitem.xml | 9 + wechat/views/wechat_micropay_views.xml | 83 +++++ wechat/views/wechat_order_views.xml | 75 ++++ wechat/views/wechat_refund_views.xml | 77 ++++ 26 files changed, 1810 insertions(+) create mode 100644 wechat/Dockerfile create mode 100644 wechat/README.rst create mode 100644 wechat/__init__.py create mode 100644 wechat/__manifest__.py create mode 100644 wechat/controllers/__init__.py create mode 100644 wechat/controllers/wechat_controllers.py create mode 100644 wechat/data/ir_sequence_data.xml create mode 100644 wechat/doc/changelog.rst create mode 100644 wechat/doc/index.rst create mode 100644 wechat/i18n/wechat.pot create mode 100644 wechat/models/__init__.py create mode 100644 wechat/models/account_journal.py create mode 100644 wechat/models/ir_config_parameter.py create mode 100644 wechat/models/wechat_micropay.py create mode 100644 wechat/models/wechat_order.py create mode 100644 wechat/models/wechat_refund.py create mode 100644 wechat/security/ir.model.access.csv create mode 100644 wechat/static/description/icon.png create mode 100644 wechat/static/description/index.html create mode 100644 wechat/tests/__init__.py create mode 100644 wechat/tests/test_wechat_order.py create mode 100644 wechat/views/account_journal_views.xml create mode 100644 wechat/views/account_menuitem.xml create mode 100644 wechat/views/wechat_micropay_views.xml create mode 100644 wechat/views/wechat_order_views.xml create mode 100644 wechat/views/wechat_refund_views.xml diff --git a/wechat/Dockerfile b/wechat/Dockerfile new file mode 100644 index 0000000000..24d3c3b202 --- /dev/null +++ b/wechat/Dockerfile @@ -0,0 +1,10 @@ +# Use this docker for development +FROM itprojectsllc/install-odoo:12.0-dev + +USER root + +RUN pip install wechatpy[cryptography] +RUN pip install wdb requests-mock # those are included in base image now and can be removed +RUN pip install python-alipay-sdk # it's for alipay modules, but we install it here for convinience + +USER odoo diff --git a/wechat/README.rst b/wechat/README.rst new file mode 100644 index 0000000000..b7fd8dbf2c --- /dev/null +++ b/wechat/README.rst @@ -0,0 +1,89 @@ +.. image:: https://itpp.dev/images/infinity-readme.png + :alt: Tested and maintained by IT Projects Labs + :target: https://itpp.dev + +============ + WeChat API +============ + +Basic tools to integrate Odoo and WeChat. + +.. contents:: + :local: + +Payment methods +=============== + +Quick Pay (micropay) +-------------------- + +Buyer presents the pay code, Vendor scans the code to finish the transaction. + +Native Payment (QR Code Payment) +-------------------------------- + +The Vendor gets one-time url and shows it to Buyer as a QR Code, Buyer scans to finish the transaction. + +Official Account Payment +------------------------ + +There are two types of usage: + +* **In-App Web-based Payment** -- The Payer opens the Vendor's HTML5 pages on their WeChat and calls the WeChat payment module via the JSAPI interface to pay their transaction. Client side of this process (i.e. web pages) is not supported. While it could be implemented as additional module, we recommend to develop *Mini programs* instead. +* **Mini program** -- an application as a part of WeChat App is created via *WeChat Developer tools*. + +In-App Payment +-------------- + +This payment way is only for native mobile application. This module provides server part of the process. + +WeChat Documentation & tools +============================ + +Sandbox & Debugging +------------------- + +* API Debug Console https://open.wechat.com/cgi-bin/newreadtemplate?t=overseas_open/docs/oa/basic-info/debug-console +* Creating Test Accounts https://admin.wechat.com/debug/cgi-bin/sandbox?t=sandbox/login + + * Note: it may not work from non-chinese IP addresses + * You will get ``appid`` and ``appsecret`` values + * To work with WeChat payments you also need Merchant ID, which this sandbox + doesn't provide. It seems, that to work with Payments you need a real + account and use *sandbox* mode (*System Parameter* ``wechat.sandbox``). + +Payments +-------- + +* https://pay.weixin.qq.com/wechatpay_guide/help_docs.shtml + +Debugging +========= + +Local Debug +----------- + +To debug UI, create *System Parameter* ``wechat.local_sandbox`` with value ``1``. All requests to wechat will return fake result without making a request. + +Native Payments debugging +------------------------- + +* It seems that in sandbox mode it's allowed to use only prices ``1.01`` and ``1.02``. + +Questions? +========== + +To get an assistance on this module contact us by email :arrow_right: help@itpp.dev + +Contributors +============ +* `Ivan Yelizariev `__ + + +Further information +=================== + +Odoo Apps Store: https://apps.odoo.com/apps/modules/13.0/wechat/ + + +Tested on `Odoo 12.0 `_ diff --git a/wechat/__init__.py b/wechat/__init__.py new file mode 100644 index 0000000000..91b99506b9 --- /dev/null +++ b/wechat/__init__.py @@ -0,0 +1,3 @@ +# License MIT (https://opensource.org/licenses/MIT). +from . import models +from . import controllers diff --git a/wechat/__manifest__.py b/wechat/__manifest__.py new file mode 100644 index 0000000000..a924757a95 --- /dev/null +++ b/wechat/__manifest__.py @@ -0,0 +1,29 @@ +# Copyright 2018 Ivan Yelizariev +# Copyright 2018 Dinar Gabbasov +# License MIT (https://opensource.org/licenses/MIT). +{ + "name": """WeChat API""", + "summary": """Technical module to integrate Odoo with WeChat""", + "category": "Hidden", + "images": [], + "version": "13.0.1.0.1", + "application": False, + "author": "IT-Projects LLC, Ivan Yelizariev", + "support": "help@itpp.dev", + "website": "https://github.com/itpp-labs/pos-addons#readme", + "license": "Other OSI approved licence", # MIT + "depends": ["product", "account", "qr_payments"], + "external_dependencies": {"python": ["wechatpy"], "bin": []}, + "data": [ + "views/account_menuitem.xml", + "views/wechat_micropay_views.xml", + "views/wechat_order_views.xml", + "views/wechat_refund_views.xml", + "views/account_journal_views.xml", + "data/ir_sequence_data.xml", + "security/ir.model.access.csv", + ], + "qweb": [], + "auto_install": False, + "installable": False, +} diff --git a/wechat/controllers/__init__.py b/wechat/controllers/__init__.py new file mode 100644 index 0000000000..83cc5acfe4 --- /dev/null +++ b/wechat/controllers/__init__.py @@ -0,0 +1,2 @@ +# License MIT (https://opensource.org/licenses/MIT). +from . import wechat_controllers diff --git a/wechat/controllers/wechat_controllers.py b/wechat/controllers/wechat_controllers.py new file mode 100644 index 0000000000..b845a7da96 --- /dev/null +++ b/wechat/controllers/wechat_controllers.py @@ -0,0 +1,37 @@ +# Copyright 2018 Ivan Yelizariev +# Copyright 2018 Dinar Gabbasov +# License MIT (https://opensource.org/licenses/MIT). +import logging + +from lxml import etree + +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class WechatController(http.Controller): + @http.route( + "/wechat/callback", methods=["POST"], auth="public", type="http", csrf=False + ) + def wechat_callback(self): + xml_raw = request.httprequest.get_data().decode(request.httprequest.charset) + _logger.debug( + "/wechat/callback request data: %s\nheaders %s: ", + xml_raw, + request.httprequest.headers, + ) + + # convert xml to object + xml = etree.fromstring(xml_raw) + data = {} + for child in xml: + data[child.tag] = child.text + + res = request.env["wechat.order"].sudo().on_notification(data) + + if res is not False: + return """""" + else: + return """""" diff --git a/wechat/data/ir_sequence_data.xml b/wechat/data/ir_sequence_data.xml new file mode 100644 index 0000000000..9876aca2bd --- /dev/null +++ b/wechat/data/ir_sequence_data.xml @@ -0,0 +1,58 @@ + + + + + WeChat Micropay + wechat.micropay + WMP- + 3 + + + + WeChat Order + wechat.order + WO- + 3 + + + + WeChat Refund + wechat.refund + WR- + 3 + + + + wechat.local_sandbox + + + + wechat.app_id + + + + wechat.app_secret + + + + wechat.mch_id + + + + wechat.sub_mch_id + + + + wechat.sandbox + + + + wechat.mch_cert + + + + wechat.mch_key + + + diff --git a/wechat/doc/changelog.rst b/wechat/doc/changelog.rst new file mode 100644 index 0000000000..89383f6ffb --- /dev/null +++ b/wechat/doc/changelog.rst @@ -0,0 +1,9 @@ +`1.0.1` +------- + +**Improvement:** Automatic keys creation after module installation + +`1.0.0` +------- + +**Init version** diff --git a/wechat/doc/index.rst b/wechat/doc/index.rst new file mode 100644 index 0000000000..839b6b1a90 --- /dev/null +++ b/wechat/doc/index.rst @@ -0,0 +1,73 @@ +============ + WeChat API +============ + +.. contents:: + :local: + +Installation +============ + +* Install `wechatpy library`__:: + + pip install wechatpy + pip install wechatpy[cryptography] + + # to update existing installation use + pip install -U wechatpy + +* Be sure that your server available for requests from outside world (i.e. it shall not be avaialble in local network only) + +Multi database +-------------- + +If you have several databases, you need to check that all requests are sent to the desired database. The notification request from the WeChat cloud does not contain session cookies. So, if Odoo cannot determine which database to use, it will return a 404 error (Page not found). +In order for the requests to send to the desired database, you need to configure `dbfilter `__. + +WeChat APP +========== + +Check WeChat documentation or contact WeChat Support about obtaining APP credentials. + +Configuration +============= + +Credentials +----------- + +* `Activate Developer Mode `__ +* Open menu ``[[ Settings ]] >> Parameters >> System Parameters`` +* Create following parameters + + * ``wechat.app_id`` + * ``wechat.app_secret`` + * ``wechat.mch_id`` -- *Vendor ID* + * ``wechat.sub_mch_id`` -- *Sub Vendor ID* + * ``wechat.sandbox`` -- set to ``0`` or delete to disable. Any other value to means that sandbox is activated. + * ``wechat.mch_cert``, ``wechat.mch_key`` -- **path** to key and certificate files at server. Example of values: + + * ``wechat.mch_cert``: ``/path/to/apiclient_cert.pem`` + * ``wechat.mch_key``: ``/path/to/apiclient_key.pem`` + + * ``wechat.payment_result_notification_url`` -- optional. Use it if doesn't work automatiically. The url must be ``http(s)://YOUR_HOST/wechat/callback``. + +Internal Numbers +---------------- + +If you get error ``invalid out_trade_no``, it means that you use the same +credentials in new database and odoo sends Wechat Order IDs that were previously +used in another system. To resolve this do as following: + +* Go to ``[[ Settings ]] >> Technical >> Sequence & Identifiers >> Sequences`` +* Find record *WeChat Order*, *Wechat Refund* or *Wechat Micropay*, depending on which request has the problem +* Change either **Prefix**, **Suffix** or **Next Number** +* If you get the error again, try to increase **Next Number** + +Wechat tracking +--------------- +Wechat records (Orders, Micropays, Refunds, etc.) can be found at ``[[ Invoicing ]] >> Configuration >> Wechat``. If you don't have that menu, you need to configure ``Show Full Accounting Features`` for your user first: + +* `Activate Developer Mode `__ +* Open menu ``[[ Settings ]] >> Users & Companies >> Users`` +* Open user you need +* Activate ``Show Full Accounting Features`` diff --git a/wechat/i18n/wechat.pot b/wechat/i18n/wechat.pot new file mode 100644 index 0000000000..66bcc8c062 --- /dev/null +++ b/wechat/i18n/wechat.pot @@ -0,0 +1,428 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * wechat +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_order__trade_type +msgid "\n" +"* Official Account Payment -- Mini Program Payment or In-App Web-based Payment\n" +"* Native Payment -- Customer scans QR for specific order and confirm payment\n" +"* In-App Payment -- payments in native mobile applications\n" +" " +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_micropay__total_fee +#: model:ir.model.fields,help:wechat.field_wechat_order__total_fee +#: model:ir.model.fields,help:wechat.field_wechat_refund__refund_fee +msgid "Amount in cents" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__description +msgid "Body" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__category +msgid "Category" +msgstr "" + +#. module: wechat +#: selection:wechat.refund,state:0 +msgid "Completed" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__create_uid +#: model:ir.model.fields,field_description:wechat.field_wechat_order__create_uid +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__create_uid +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__create_uid +msgid "Created by" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__create_date +#: model:ir.model.fields,field_description:wechat.field_wechat_order__create_date +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__create_date +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__create_date +msgid "Created on" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order__currency_id +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__currency_id +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__currency_id +msgid "Currency" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__display_name +#: model:ir.model.fields,field_description:wechat.field_wechat_order__display_name +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__display_name +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__display_name +msgid "Display Name" +msgstr "" + +#. module: wechat +#: selection:wechat.refund,state:0 +msgid "Draft" +msgstr "" + +#. module: wechat +#: selection:wechat.micropay,state:0 +#: selection:wechat.order,state:0 +#: selection:wechat.refund,state:0 +msgid "Error" +msgstr "" + +#. module: wechat +#: code:addons/wechat/models/wechat_order.py:144 +#, python-format +msgid "Error on sending request to WeChat: %s" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__id +#: model:ir.model.fields,field_description:wechat.field_wechat_order__id +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__id +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__id +msgid "ID" +msgstr "" + +#. module: wechat +#: selection:wechat.order,trade_type:0 +msgid "In-App Payment" +msgstr "" + +#. module: wechat +#: model:ir.model,name:wechat.model_account_journal +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__journal_id +#: model:ir.model.fields,field_description:wechat.field_wechat_order__journal_id +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__journal_id +msgid "Journal" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay____last_update +#: model:ir.model.fields,field_description:wechat.field_wechat_order____last_update +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line____last_update +#: model:ir.model.fields,field_description:wechat.field_wechat_refund____last_update +msgid "Last Modified on" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__write_uid +#: model:ir.model.fields,field_description:wechat.field_wechat_order__write_uid +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__write_uid +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__write_date +#: model:ir.model.fields,field_description:wechat.field_wechat_order__write_date +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__write_date +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__write_date +msgid "Last Updated on" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order__line_ids +msgid "Line" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__micropay_id +msgid "Micropay" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__name +#: model:ir.model.fields,field_description:wechat.field_wechat_order__name +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__name +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__name +msgid "Name" +msgstr "" + +#. module: wechat +#: selection:wechat.order,trade_type:0 +msgid "Native Payment" +msgstr "" + +#. module: wechat +#: selection:wechat.micropay,state:0 +msgid "New" +msgstr "" + +#. module: wechat +#: model_terms:ir.ui.view,arch_db:wechat.micropay_search +#: model_terms:ir.ui.view,arch_db:wechat.order_search +#: model_terms:ir.ui.view,arch_db:wechat.refund_search +msgid "Non-Sandbox" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order__notification_received +msgid "Notification Received" +msgstr "" + +#. module: wechat +#: selection:wechat.order,trade_type:0 +msgid "Official Account Payment (Mini Program)" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__order_id +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__order_id +msgid "Order" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__order_ref +#: model:ir.model.fields,field_description:wechat.field_wechat_order__order_ref +msgid "Order Reference" +msgstr "" + +#. module: wechat +#: selection:wechat.micropay,state:0 +#: selection:wechat.order,state:0 +msgid "Paid" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_micropay__debug +#: model:ir.model.fields,help:wechat.field_wechat_order__debug +msgid "Payment was not made. It's only for testing purposes" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__price +msgid "Price" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_order_line__price +msgid "Price in currency units (not cents)" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__product_id +msgid "Product" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__quantity +msgid "Quantity" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__quantity_full +msgid "Quantity Value" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_order_line__quantity +msgid "Quantity as Integer (WeChat limitation)" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order__notification_result_raw +msgid "Raw Notification result" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order__order_details_raw +msgid "Raw Order" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__refund_details_raw +msgid "Raw Refund" +msgstr "" + +#. module: wechat +#: model_terms:ir.ui.view,arch_db:wechat.micropay_form +#: model_terms:ir.ui.view,arch_db:wechat.order_form +#: model_terms:ir.ui.view,arch_db:wechat.refund_form +msgid "Raw Response" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__result_raw +#: model:ir.model.fields,field_description:wechat.field_wechat_order__result_raw +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__result_raw +msgid "Raw result" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order__refund_ids +msgid "Refund" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order__refund_fee +msgid "Refund Amount" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__refund_fee +msgid "Refund Fee" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__refund_ref +msgid "Refund Reference" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_refund__debug +msgid "Refund was not made. It's only for testing purposes" +msgstr "" + +#. module: wechat +#: selection:wechat.micropay,state:0 +#: selection:wechat.order,state:0 +msgid "Refunded (part of full amount)" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_account_journal__wechat +msgid "Register for WeChat payment" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__debug +#: model:ir.model.fields,field_description:wechat.field_wechat_order__debug +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__debug +#: model_terms:ir.ui.view,arch_db:wechat.micropay_search +#: model_terms:ir.ui.view,arch_db:wechat.order_search +#: model_terms:ir.ui.view,arch_db:wechat.refund_search +msgid "Sandbox" +msgstr "" + +#. module: wechat +#: selection:account.journal,wechat:0 +msgid "Scanning customer's QR" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_order__notification_received +msgid "Set to true on receiving notifcation to avoid repeated processing" +msgstr "" + +#. module: wechat +#: selection:account.journal,wechat:0 +msgid "Showing QR to customer" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__state +#: model:ir.model.fields,field_description:wechat.field_wechat_order__state +#: model:ir.model.fields,field_description:wechat.field_wechat_refund__state +msgid "State" +msgstr "" + +#. module: wechat +#: model:ir.model,name:wechat.model_ir_config_parameter +msgid "System Parameter" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__terminal_ref +msgid "Terminal Reference" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_micropay__total_fee +#: model:ir.model.fields,field_description:wechat.field_wechat_order__total_fee +msgid "Total Fee" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order__trade_type +msgid "Trade Type" +msgstr "" + +#. module: wechat +#: model:ir.model,name:wechat.model_wechat_order +msgid "Unified Order" +msgstr "" + +#. module: wechat +#: model:ir.model,name:wechat.model_wechat_refund +msgid "Unified Refund" +msgstr "" + +#. module: wechat +#: selection:wechat.order,state:0 +msgid "Unpaid" +msgstr "" + +#. module: wechat +#: model:ir.model,name:wechat.model_wechat_micropay +msgid "WeChat Micropay" +msgstr "" + +#. module: wechat +#: model:ir.model,name:wechat.model_wechat_order_line +msgid "WeChat Order Line" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_account_journal__wechat +msgid "WeChat Payment" +msgstr "" + +#. module: wechat +#: model:ir.ui.menu,name:wechat.root_wechat_menu +msgid "Wechat" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,field_description:wechat.field_wechat_order_line__wxpay_goods_ID +msgid "Wechat Good ID" +msgstr "" + +#. module: wechat +#: model:ir.actions.act_window,name:wechat.action_wechat_micropay +#: model:ir.ui.menu,name:wechat.wechat_micropay_menu +#: model_terms:ir.ui.view,arch_db:wechat.micropay_form +msgid "Wechat Micropay" +msgstr "" + +#. module: wechat +#: model:ir.actions.act_window,name:wechat.action_wechat_order +#: model:ir.ui.menu,name:wechat.wechat_order_menu +#: model_terms:ir.ui.view,arch_db:wechat.order_form +msgid "Wechat Order" +msgstr "" + +#. module: wechat +#: model:ir.actions.act_window,name:wechat.action_wechat_refund +#: model:ir.ui.menu,name:wechat.wechat_refund_menu +#: model_terms:ir.ui.view,arch_db:wechat.refund_form +msgid "Wechat Refund" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_order_line__name +msgid "When empty, product's name is used" +msgstr "" + +#. module: wechat +#: model:ir.model.fields,help:wechat.field_wechat_micropay__terminal_ref +msgid "e.g. POS Name" +msgstr "" + diff --git a/wechat/models/__init__.py b/wechat/models/__init__.py new file mode 100644 index 0000000000..397bd5c6da --- /dev/null +++ b/wechat/models/__init__.py @@ -0,0 +1,6 @@ +# License MIT (https://opensource.org/licenses/MIT). +from . import wechat_micropay +from . import wechat_order +from . import wechat_refund +from . import ir_config_parameter +from . import account_journal diff --git a/wechat/models/account_journal.py b/wechat/models/account_journal.py new file mode 100644 index 0000000000..73d055aae3 --- /dev/null +++ b/wechat/models/account_journal.py @@ -0,0 +1,13 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +from odoo import fields, models + + +class Journal(models.Model): + _inherit = "account.journal" + + wechat = fields.Selection( + [("micropay", "Scanning customer's QR"), ("native", "Showing QR to customer")], + string="WeChat Payment", + help="Register for WeChat payment", + ) diff --git a/wechat/models/ir_config_parameter.py b/wechat/models/ir_config_parameter.py new file mode 100644 index 0000000000..f11d7070d5 --- /dev/null +++ b/wechat/models/ir_config_parameter.py @@ -0,0 +1,44 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +import logging + +from odoo import api, models + +_logger = logging.getLogger(__name__) + +try: + from wechatpy import WeChatPay +except ImportError as err: + _logger.debug(err) + + +class Param(models.Model): + + _inherit = "ir.config_parameter" + + @api.model + def get_wechat_pay_object(self): + sandbox = self.get_param("wechat.sandbox", "0") != "0" + if sandbox: + _logger.info("Sandbox Mode is used for WeChat API") + + _logger.debug( + "WeChat Credentials: app_id=%s, app_secret=%s, mch_id=%s, sub_mch_id=%s, sandbox mode is %s, cert=%s, key=%s", + self.get_param("wechat.app_id", ""), + "%s..." % self.get_param("wechat.app_secret", "")[:5], + self.get_param("wechat.mch_id", ""), + self.get_param("wechat.sub_mch_id", ""), + sandbox, + self.get_param("wechat.mch_cert", ""), + self.get_param("wechat.mch_key", ""), + ) + return WeChatPay( + self.get_param("wechat.app_id", ""), + self.get_param("wechat.app_secret", ""), + self.get_param("wechat.mch_id", ""), + sub_mch_id=self.get_param("wechat.sub_mch_id", ""), + sandbox=sandbox, + mch_cert=self.get_param("wechat.mch_cert", ""), + mch_key=self.get_param("wechat.mch_key", ""), + # TODO addd timeout? + ) diff --git a/wechat/models/wechat_micropay.py b/wechat/models/wechat_micropay.py new file mode 100644 index 0000000000..c16a8ceb16 --- /dev/null +++ b/wechat/models/wechat_micropay.py @@ -0,0 +1,108 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +import json +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class Micropay(models.Model): + + _name = "wechat.micropay" + _description = "WeChat Micropay" + _order = "id desc" + + name = fields.Char("Name", readonly=True) + order_ref = fields.Char("Order Reference", readonly=True) + terminal_ref = fields.Char( + "Terminal Reference", help="e.g. POS Name", readonly=True + ) + total_fee = fields.Integer("Total Fee", help="Amount in cents", readonly=True) + debug = fields.Boolean( + "Sandbox", + help="Payment was not made. It's only for testing purposes", + readonly=True, + ) + result_raw = fields.Text("Raw result", readonly=True) + journal_id = fields.Many2one("account.journal") + state = fields.Selection( + [ + ("draft", "New"), + ("done", "Paid"), + ("error", "Error"), + ("refunded", "Refunded (part of full amount)"), + ], + string="State", + default="draft", + ) + + @api.model + def _body(self, terminal_ref, **kwargs): + return "%s - Products" % terminal_ref + + @api.model + def create_from_qr( + self, + body, + auth_code, + pay_amount, + terminal_ref=None, + create_vals=None, + order_ref=None, + **kwargs + ): + """ + :param product_category: is used to prepare "body" + :param pay_amount: Specifies the amount to pay. The units are in currency units (not cents) + :param create_vals: extra args to pass on record creation + """ + debug = self.env["ir.config_parameter"].get_param("wechat.local_sandbox") == "1" + total_fee = int(100 * pay_amount) + vals = { + "journal_id": kwargs["journal_id"], + "debug": debug, + "terminal_ref": terminal_ref, + "order_ref": order_ref, + "total_fee": total_fee, + } + if create_vals: + vals.update(create_vals) + record = self.create(vals) + + if debug: + _logger.info( + "SANDBOX is activated. Request to wechat servers are not sending" + ) + # Dummy Data. Change it to try different scenarios + result_json = { + "return_code": "SUCCESS", + "result_code": "SUCCESS", + "openid": "123", + "total_fee": total_fee, + "order_ref": order_ref, + } + if self.env.context.get("debug_micropay_response"): + result_json = self.env.context.get("debug_micropay_response") + else: + wpay = self.env["ir.config_parameter"].get_wechat_pay_object() + # TODO: we probably have make cr.commit() before making request to + # be sure that we save data before sending request to avoid + # situation when order is sent to wechat server, but was not saved + # in our server for any reason + + result_json = wpay.micropay.create( + body, total_fee, auth_code, out_trade_no=record.name + ) + + result_raw = json.dumps(result_json) + _logger.debug("result_raw: %s", result_raw) + vals = {"result_raw": result_raw, "state": "done"} + record.write(vals) + return record + + @api.model + def create(self, vals): + vals["name"] = self.env["ir.sequence"].next_by_code("wechat.micropay") + return super(Micropay, self).create(vals) diff --git a/wechat/models/wechat_order.py b/wechat/models/wechat_order.py new file mode 100644 index 0000000000..1e82eb8bdc --- /dev/null +++ b/wechat/models/wechat_order.py @@ -0,0 +1,290 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +import json +import logging + +from odoo import api, fields, models +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + +try: + from wechatpy.exceptions import WeChatPayException +except ImportError as err: + _logger.debug(err) + + +PAYMENT_RESULT_NOTIFICATION_URL = "wechat/callback" +SUCCESS = "SUCCESS" + + +class WeChatOrder(models.Model): + """Records with order information and payment status. + + Can be used for different types of Payments. See description of trade_type field.""" + + _name = "wechat.order" + _description = "Unified Order" + _order = "id desc" + + name = fields.Char("Name", readonly=True) + trade_type = fields.Selection( + [ + ("JSAPI", "Official Account Payment (Mini Program)"), + ("NATIVE", "Native Payment"), + ("APP", "In-App Payment"), + ], + help=""" +* Official Account Payment -- Mini Program Payment or In-App Web-based Payment +* Native Payment -- Customer scans QR for specific order and confirm payment +* In-App Payment -- payments in native mobile applications + """, + ) + + order_ref = fields.Char("Order Reference", readonly=True) + total_fee = fields.Integer("Total Fee", help="Amount in cents", readonly=True) + state = fields.Selection( + [ + ("draft", "Unpaid"), + ("done", "Paid"), + ("error", "Error"), + ("refunded", "Refunded (part of full amount)"), + ], + string="State", + default="draft", + ) + # terminal_ref = fields.Char('Terminal Reference', help='e.g. POS Name', readonly=True) + debug = fields.Boolean( + "Sandbox", + help="Payment was not made. It's only for testing purposes", + readonly=True, + ) + order_details_raw = fields.Text("Raw Order", readonly=True) + result_raw = fields.Text("Raw result", readonly=True) + notification_result_raw = fields.Text("Raw Notification result", readonly=True) + currency_id = fields.Many2one( + "res.currency", default=lambda self: self.env.user.company_id.currency_id + ) + notification_received = fields.Boolean( + help="Set to true on receiving notifcation to avoid repeated processing", + default=False, + ) + journal_id = fields.Many2one("account.journal") + refund_fee = fields.Integer("Refund Amount", compute="_compute_refund_fee") + line_ids = fields.One2many("wechat.order.line", "order_id") + refund_ids = fields.One2many("wechat.refund", "order_id") + + @api.depends("refund_ids.refund_fee", "refund_ids.state") + def _compute_refund_fee(self): + for r in self: + r.refund_fee = sum( + [ref.refund_fee for ref in r.refund_ids if ref.state == "done"] + ) + + def _body(self): + """Example of result: + + {"goods_detail": [ + { + "goods_id": "iphone6s_16G", + "wxpay_goods_id": "100 1", + "goods_name": "iPhone 6s 16G", + "goods_num": 1, + "price": 100, + "goods_category": "123456", + "body": "苹果手机", + }, + { + "goods_id": "iphone6s_3 2G", + "wxpay_goods_id": "100 2", + "goods_name": "iPhone 6s 32G", + "quantity": 1, + "price": 200, + "goods_category": "123789", + } + ]}""" + self.ensure_one() + rendered_lines = [] + order_body = [] + for line in self.line_ids: + name = line.name or line.product_id.name + body = name + if line.quantity_full != "1": + body = "{} {}".format(body, line.quantity_full) + order_body.append(body) + rline = { + "goods_id": str(line.product_id.id), + "goods_name": name, + "goods_num": line.quantity, + "price": line.get_fee(), + "body": body, + } + if line.category: + rline["category"] = line.category + + if line.wxpay_goods_ID: + rline["wxpay_goods_id"] = line.wxpay_goods_id + + rendered_lines.append(rline) + detail = {"goods_detail": rendered_lines} + order_body = "; ".join(order_body) + + return order_body, detail + + def _total_fee(self): + self.ensure_one() + total_fee = sum([line.get_fee() for line in self.line_ids]) + return total_fee + + def _notify_url(self): + url = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("wechat.payment_result_notification_url") + ) + if url: + return url + + base = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + return "{base}/{path}".format(base=base, path=PAYMENT_RESULT_NOTIFICATION_URL) + + @api.model + def create_qr(self, lines, **kwargs): + try: + order, code_url = self._create_qr(lines, **kwargs) + except WeChatPayException as e: + return { + "error": _("Error on sending request to WeChat: %s") % e.response.text + } + return {"code_url": code_url} + + @api.model + def _create_qr(self, lines, create_vals=None, pay_amount=None, **kwargs): + """Native Payment + + :param lines: list of dictionary + :param pay_amount: amount in currency (not cents) + """ + debug = self.env["ir.config_parameter"].get_param("wechat.local_sandbox") == "1" + vals = { + "trade_type": "NATIVE", + "line_ids": [(0, 0, data) for data in lines], + "order_ref": kwargs.get("order_ref"), + "journal_id": kwargs.get("journal_id"), + "debug": debug, + } + if create_vals: + vals.update(create_vals) + order = self.create(vals) + if pay_amount: + # TODO: make a single method for this + total_fee = int(100 * pay_amount) + else: + total_fee = order._total_fee() + if debug: + _logger.info( + "SANDBOX is activated. Request to wechat servers is not sending" + ) + # Dummy Data. Change it to try different scenarios + result_json = { + "return_code": "SUCCESS", + "result_code": "SUCCESS", + "openid": "123", + "code_url": "weixin://wxpay/s/An4baqw", + } + if self.env.context.get("debug_wechat_order_response"): + result_json = self.env.context.get("debug_wechat_order_response") + else: + body, detail = order._body() + wpay = self.env["ir.config_parameter"].get_wechat_pay_object() + # TODO: we probably have make cr.commit() before making request to + # be sure that we save data before sending request to avoid + # situation when order is sent to wechat server, but was not saved + # in our server for any reason + _logger.debug( + "Unified order:\n total_fee: %s\n body: %s\n, detail: \n %s", + total_fee, + body, + detail, + ) + result_json = wpay.order.create( + "NATIVE", + body, + total_fee, + self._notify_url(), + out_trade_no=order.name, + detail=detail, + # TODO fee_type=record.currency_id.name + ) + + result_raw = json.dumps(result_json) + _logger.debug("result_raw: %s", result_raw) + vals = {"result_raw": result_raw, "total_fee": total_fee} + order.write(vals) + code_url = result_json["code_url"] + return order, code_url + + def on_notification(self, data): + """ + return updated record + """ + # check signature + wpay = self.env["ir.config_parameter"].get_wechat_pay_object() + if not wpay.check_signature(data): + _logger.warning("Notification Signature is not valid:\n", data) + return False + + order_name = data.get("out_trade_no") + order = None + if order_name: + order = self.search([("name", "=", order_name)]) + if not order: + _logger.warning("Order %s from notification is not found", order.id) + return False + + # check for duplicates + if order.notification_received: + _logger.warning("Notifcation duplicate is received: %s", order) + return None + + vals = { + "notification_result_raw": json.dumps(data), + "notification_received": True, + } + if not (data["return_code"] == SUCCESS and data["result_code"] == SUCCESS): + vals["state"] = "error" + + else: + vals["state"] = "done" + + order.write(vals) + return order + + @api.model + def create(self, vals): + vals["name"] = self.env["ir.sequence"].next_by_code("wechat.order") + return super(WeChatOrder, self).create(vals) + + +class WeChatOrderLine(models.Model): + _name = "wechat.order.line" + _description = "WeChat Order Line" + + name = fields.Char("Name", help="When empty, product's name is used") + description = fields.Char("Body") + product_id = fields.Many2one("product.product", required=True) + wxpay_goods_ID = fields.Char("Wechat Good ID") + price = fields.Monetary( + "Price", required=True, help="Price in currency units (not cents)" + ) + currency_id = fields.Many2one("res.currency", related="order_id", string="Currency") + quantity = fields.Integer( + "Quantity", default=1, help="Quantity as Integer (WeChat limitation)" + ) + quantity_full = fields.Char("Quantity Value", default="1") + category = fields.Char("Category") + order_id = fields.Many2one("wechat.order", string="Order") + + def get_fee(self): + self.ensure_one() + return int(100 * (self.price or self.product_id.price)) diff --git a/wechat/models/wechat_refund.py b/wechat/models/wechat_refund.py new file mode 100644 index 0000000000..a2eb9b03b1 --- /dev/null +++ b/wechat/models/wechat_refund.py @@ -0,0 +1,77 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +import json +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + +SUCCESS = "SUCCESS" + + +class WeChatRefund(models.Model): + """Records with refund information and payment status. + + Can be used for different types of Payments. See description of trade_type field.""" + + _name = "wechat.refund" + _description = "Unified Refund" + _order = "id desc" + + name = fields.Char("Name", readonly=True) + refund_ref = fields.Char("Refund Reference", readonly=True) + refund_fee = fields.Integer("Refund Fee", help="Amount in cents", readonly=True) + state = fields.Selection( + [("draft", "Draft"), ("done", "Completed"), ("error", "Error")], + string="State", + default="draft", + ) + debug = fields.Boolean( + "Sandbox", + help="Refund was not made. It's only for testing purposes", + readonly=True, + ) + refund_details_raw = fields.Text("Raw Refund", readonly=True) + result_raw = fields.Text("Raw result", readonly=True) + currency_id = fields.Many2one( + "res.currency", default=lambda self: self.env.user.company_id.currency_id + ) + order_id = fields.Many2one("wechat.order") + micropay_id = fields.Many2one("wechat.micropay") + journal_id = fields.Many2one("account.journal") + + @api.model + def create(self, vals): + vals["name"] = self.env["ir.sequence"].next_by_code("wechat.refund") + return super(WeChatRefund, self).create(vals) + + def action_confirm(self): + self.ensure_one() + debug = self.env["ir.config_parameter"].get_param("wechat.local_sandbox") == "1" + wpay = self.env["ir.config_parameter"].get_wechat_pay_object() + record = self.order_id or self.micropay_id + if debug: + _logger.info( + "SANDBOX is activated. Request to wechat servers is not sending" + ) + # Dummy Data. Change it to try different scenarios + if self.env.context.get("debug_wechat_refund_response"): + result_raw = self.env.context.get("debug_wechat_order_response") + else: + result_raw = { + "return_code": "SUCCESS", + "result_code": "SUCCESS", + "transaction_id": "12177525012014", + "refund_id": "12312122222", + } + + else: + wpay = self.env["ir.config_parameter"].get_wechat_pay_object() + result_raw = wpay.refund.apply( + record.total_fee, self.refund_fee, self.name, out_trade_no=record.name + ) + + vals = {"result_raw": json.dumps(result_raw), "state": "done"} + self.write(vals) + record.state = "refunded" diff --git a/wechat/security/ir.model.access.csv b/wechat/security/ir.model.access.csv new file mode 100644 index 0000000000..62300484fa --- /dev/null +++ b/wechat/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_wechat_order_line,access_wechat_order_line,model_wechat_order_line,base.group_user,1,0,0,0 +access_wechat_micropay,access_wechat_micropay,model_wechat_micropay,base.group_user,1,0,0,0 +access_wechat_order,access_wechat_order,model_wechat_order,base.group_user,1,0,0,0 +access_wechat_refund,access_wechat_refund,model_wechat_refund,base.group_user,1,0,0,0 diff --git a/wechat/static/description/icon.png b/wechat/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b43a0a135f903a4c0c401f03a8690fbbd021a2ab GIT binary patch literal 3035 zcmV<13ncW3P))ueQQHJ1Rg98mIup&<-zh`c?djM9xM-*2g`%y!SWDzusm2EEDx3k%Y)@1 z@L+kcJXjvA!P)%o{8@9>FT5*fw0hgsc`>kZ>zPoCrz~G;M%wh{vnQ0~t0Ouo+OX8y zhTV}O`(vk%|Nd`EGA56^<&*^l80F5*XUA9QYYVkB*EZPqpZ?rzUO^Vxb+~r(C!Njh zw}i5oaPj(ulYdrVkm_y^JYHabirzV=a3ZJoddeVC7Mj8U^&=UR$L2rvg9x4TM&^vx zO;v;jzJG82Wbnf}6+8wVE5S%IWD7gUEn$YASMDF6$6=M+|Hj`3EiBYgw06FdPjKB| z2QpgS_8#0atkeE0re=tX!6o6Ru)f@qJV@mfO|o| z&b)>Khn-s=lVbTW8&@8GY~Ke!`*!SM`{L;J7-GzQ2c6>w>e?Dx`H1b2#dBx=w8&-z#6`SnAFTDe4K2wj zi~*^0S1#QDi-NKDXWIy>V)cgL2fL>fes9s*CA?nly7a}dhPI~Gk(udJ=ggS(M6oZ) z_i@FJmXl3zwD~R~jH5gLm8i;s)X~oR(f|El-t1|S`fPb&BMMqjws_J*Qz~EGf*G1R z+&d*#K7H+(m$sB|WIxhS?WfN;G!e&@C8Vxgy;6TT1dfqe=_ti&eQ4^jRi)S+Ea+w( z2Z=!tWSC*q`uCEDC1+2}vC8vUGz@8MNY@^JEP$!Z)B|MAHezX^4wC8s8ygEx_i)W8R?N1mLEz7(mF zWeapieyI8Yc1kxkc2;aI-}&bDLp28gr~2HUIz0XIh*87tOuRVDM2hJlhe)t+*i4mm zFf1t=9TtsP^4ilcu}?^Wh;aG)s`lux7=P*R7gc1xjMInJDWE z8yw)R)FOz`cpKKWsp$8P8&h-_!MZDFG@&^Ohm`T=tY7%!rStl7qSzgNT^S(zkvc2l z@U^ZI36L8~b!pu6@uYi_#H3Fe!>mvT8sYle$J|(-eL`b@VvL)AfTW7n%s1*J|2$?Y zu;7UotX_=apaRyoh;#17gh8pvD~W}XVPnrn$clKiABf|otgde^Z zM>w+Z%eh1e`aD_D2pU)cmCvR#YoFc+Qg5yNgDOIB3M(TEqW+Uu`}Bg36X~J9-=yZP z{<(yU{CY@<`5gbOPF;ECX2_~LCFbqg$IJA!%ml0Y-2fR3eI%HIg^@^idvtWiWl*fK zZkS?*2v+}9XL`HY5r+uAUJ9B;!f+BY@V(X;q z>({qNr5Wj7l5bXN@sED_6MaB>lDm*06U5UW5Jc)6XF&FaiySdXl`Wp|;3Oh`lxPY^ z(#DP=g>J2D>N)K%ZrjO@&UM;}%TIkP4EU9@%yav(?Hwc_~=NCptl zlq{#?tM+AYt)4J#l66>4X*tW*zzS-gv@lx!BflzDd8%WH6Es60pooz2dNB+H{~NdQ=m}RGGhxZT8x@W>RZbV+K}ib8bzv zGea`rhV!Sxl9I_I0Xz}T#VCGfXY$iowjjb0Rk30khJ)*_C}2&vd?0e}pO(JDE7qzO ziheX~mXTSD`Cf)?u02)zj)Qln{WhcsrU_3p=aTV2o_)Ek0-MI9*bBh?t&WWT z49<$l4nyg8G-5+446HuZf~}1NMY0@bi_98#VC*#&yduI9mY{5Aab3y6D^^@VMj9_e^f|`B2?*;D=)VdF!SfFUim`^Oxw8kReeBsNw&yA z9ah*h`(}U4*lETS&G9cQhi%3JcVm2nh?pc4AR=wG(O*2k3f{u=@s$gt6}L?1Xy5hcy7rb z<)i9%156M9g}6+Lvo}LAYEx|kKh*;(FIT@FJxUL`8yWT^@$$%Tj0>RxB6v;-~cEXzCxotxOD9gzq<1 z)k9o14_n#uvbNBk|LlKtmlK5tM;>NVi<-KvZ*8POq9*LCkECS4@{;9|iW3Z2^ZJLu z7pfd8(F}cX%y*(bXx*oWd`ZSm#q%3RW~D1fBmPVFgO$5k8C&X`s$PHZrUt*b@bznW0T!)oTDO1%7p8)UZ3V zhiCbmY>p_)6d&Q*F%YC=ph@bX7Tu6N2Be&F@G)+&A>|tc|JNaAq!J!hEJ-CctQeC@ zU|6vym8h`dLMkC)#fwzp!HOfP1cMb{Qa;!IODJp0O1V2T +
+
+

WeChat(微信) API

+

Technical module to integrate Odoo with WeChat

+
+
+ + +
+
+
+ +
+ Key features: +
    + +
  • + + Buyer scans merchant's QR code to pay order (扫码支付) +
  • + +
  • + + Merchant scans buyer's QR code to send order payment information (刷卡支付) +
  • + +
+
+ +
+
+
+ +
+
+
+ +
+ For POS integration check our module WeChat Payments in POS +
+ +
+
+
+ +
+
+
+

Need a custom miniprogram for WeChat (微信小程序)?

+

Contact our partner in China

+ +
+
+
+ +
+
+
+

Need our service?

+

Contact us by email or fill out request form

+ +
+
+
+
+ Tested on Odoo
12.0 community +
+ +
+
+
+
diff --git a/wechat/tests/__init__.py b/wechat/tests/__init__.py new file mode 100644 index 0000000000..f1b93aa655 --- /dev/null +++ b/wechat/tests/__init__.py @@ -0,0 +1,2 @@ +# License MIT (https://opensource.org/licenses/MIT). +from . import test_wechat_order diff --git a/wechat/tests/test_wechat_order.py b/wechat/tests/test_wechat_order.py new file mode 100644 index 0000000000..df31de02d7 --- /dev/null +++ b/wechat/tests/test_wechat_order.py @@ -0,0 +1,157 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +import logging + +from odoo import api +from odoo.tests.common import HttpCase + +try: + from unittest.mock import patch +except ImportError: + from mock import patch + + +_logger = logging.getLogger(__name__) + + +class TestWeChatOrder(HttpCase): + at_install = True + post_install = True + + def setUp(self): + super(TestWeChatOrder, self).setUp() + self.phantom_env = api.Environment(self.registry.test_cr, self.uid, {}) + + self.Order = self.phantom_env["wechat.order"] + self.Refund = self.phantom_env["wechat.refund"] + self.product1 = self.phantom_env["product.product"].create({"name": "Product1"}) + self.product2 = self.phantom_env["product.product"].create({"name": "Product2"}) + + patcher = patch("wechatpy.WeChatPay.check_signature", wraps=lambda *args: True) + patcher.start() + self.addCleanup(patcher.stop) + + 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": "網路白目哈哈", + }, + ] + + def _patch_post(self, post_result): + def post(url, data): + self.assertIn(url, post_result) + _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) + + def _create_order(self): + post_result = { + "pay/unifiedorder": { + "code_url": "weixin://wxpay/s/An4baqw", + "trade_type": "NATIVE", + "result_code": "SUCCESS", + } + } + self._patch_post(post_result) + order, code_url = self.Order._create_qr(self.lines, total_fee=300) + self.assertEqual(order.state, "draft", "Just created order has wrong state") + return order + + def test_native_payment(self): + """ Create QR, emulate payment, make refund """ + + order = self._create_order() + + # emulate notification + notification = { + "return_code": "SUCCESS", + "result_code": "SUCCESS", + "out_trade_no": order.name, + } + handled = self.Order.on_notification(notification) + self.assertTrue( + handled, "Notification was not handled (error in checking for duplicates?)" + ) + self.assertEqual( + order.state, + "done", + "Order's state is not changed after notification about update", + ) + + # refund + post_result = { + "secapi/pay/refund": {"trade_type": "NATIVE", "result_code": "SUCCESS"} + } + self._patch_post(post_result) + + refund_fee = 100 + refund_vals = { + "order_id": order.id, + "total_fee": order.total_fee, + "refund_fee": refund_fee, + } + refund = self.Refund.create(refund_vals) + self.assertEqual( + order.refund_fee, + 0, + "Order's refund ammout is not zero when refund is not confirmed", + ) + refund.action_confirm() + self.assertEqual( + refund.state, + "done", + "Refund's state is not changed after refund is confirmed", + ) + self.assertEqual( + order.state, + "refunded", + "Order's state is not changed after refund is confirmed", + ) + self.assertEqual( + order.refund_fee, refund_fee, "Order's refund amount is computed wrongly" + ) + + refund = self.Refund.create(refund_vals) + refund.action_confirm() + self.assertEqual( + order.refund_fee, + 2 * refund_fee, + "Order's refund amount is computed wrongly", + ) + + def test_notification_duplicates(self): + order = self._create_order() + + # simulate notification with failing request + notification = { + "return_code": "SUCCESS", + "result_code": "FAIL", + "error_code": "SYSTEMERR", + # 'transaction_id': '121775250120121775250120', + "out_trade_no": order.name, + } + handled = self.Order.on_notification(notification) + self.assertTrue( + handled, "Notification was not handled (error in checking for duplicates?)" + ) + handled = self.Order.on_notification(notification) + self.assertFalse( + handled, "Duplicate was not catched and handled as normal notificaiton" + ) diff --git a/wechat/views/account_journal_views.xml b/wechat/views/account_journal_views.xml new file mode 100644 index 0000000000..ce20b207c3 --- /dev/null +++ b/wechat/views/account_journal_views.xml @@ -0,0 +1,15 @@ + + + + + wechat.account_journal_form + account.journal + + + + + + + + diff --git a/wechat/views/account_menuitem.xml b/wechat/views/account_menuitem.xml new file mode 100644 index 0000000000..94acf5a29f --- /dev/null +++ b/wechat/views/account_menuitem.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/wechat/views/wechat_micropay_views.xml b/wechat/views/wechat_micropay_views.xml new file mode 100644 index 0000000000..7ee4247a55 --- /dev/null +++ b/wechat/views/wechat_micropay_views.xml @@ -0,0 +1,83 @@ + + + + + + + wechat.micropay.form + wechat.micropay + +
+
+ +
+ +
+

+ +

+
+ + + + + + + + + + + +
+
+
+
+ + wechat.micropay.list + wechat.micropay + + + + + + + + + + + wechat.micropay.search + wechat.micropay + + + + + + + + + + + Wechat Micropay + wechat.micropay + tree,form + + +
diff --git a/wechat/views/wechat_order_views.xml b/wechat/views/wechat_order_views.xml new file mode 100644 index 0000000000..8f776413a5 --- /dev/null +++ b/wechat/views/wechat_order_views.xml @@ -0,0 +1,75 @@ + + + + + wechat.order.form + wechat.order + +
+
+ +
+ +
+

+ +

+
+ + + + + + + + + + +
+
+
+
+ + wechat.order.list + wechat.order + + + + + + + + + + wechat.order.search + wechat.order + + + + + + + + + + + Wechat Order + wechat.order + tree,form + + +
diff --git a/wechat/views/wechat_refund_views.xml b/wechat/views/wechat_refund_views.xml new file mode 100644 index 0000000000..ea233abcff --- /dev/null +++ b/wechat/views/wechat_refund_views.xml @@ -0,0 +1,77 @@ + + + + + wechat.refund.form + wechat.refund + +
+
+ +
+ +
+

+ +

+
+ + + + + + + + + + + + +
+
+
+
+ + wechat.refund.list + wechat.refund + + + + + + + + + + wechat.refund.search + wechat.refund + + + + + + + + + + + Wechat Refund + wechat.refund + tree,form + + +
From cdb9a12b41af1648ffe3561a724aa45dd8544dc4 Mon Sep 17 00:00:00 2001 From: Mitchell Admin Date: Wed, 11 Aug 2021 11:09:35 +0000 Subject: [PATCH 2/3] :arrow_up::one::four: OCA/odoo-module-migrator close #1444 > Made via .github/workflows/DINAR-PORT.yml --- wechat/__manifest__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wechat/__manifest__.py b/wechat/__manifest__.py index a924757a95..4243e9d2e3 100644 --- a/wechat/__manifest__.py +++ b/wechat/__manifest__.py @@ -6,7 +6,7 @@ "summary": """Technical module to integrate Odoo with WeChat""", "category": "Hidden", "images": [], - "version": "13.0.1.0.1", + "version": "14.0.1.0.1", "application": False, "author": "IT-Projects LLC, Ivan Yelizariev", "support": "help@itpp.dev", @@ -25,5 +25,5 @@ ], "qweb": [], "auto_install": False, - "installable": False, + "installable": True, } From 334931a6f24bdcc0f5d24932f433198fca240f36 Mon Sep 17 00:00:00 2001 From: Mitchell Admin Date: Wed, 11 Aug 2021 11:10:53 +0000 Subject: [PATCH 3/3] :rainbow: pre-commit > Made via .github/workflows/DINAR-PORT.yml --- requirements.txt | 2 ++ setup/wechat/odoo/addons/wechat | 1 + setup/wechat/setup.py | 6 ++++++ 3 files changed, 9 insertions(+) create mode 100644 requirements.txt create mode 120000 setup/wechat/odoo/addons/wechat create mode 100644 setup/wechat/setup.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..a1a146541d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +wechatpy diff --git a/setup/wechat/odoo/addons/wechat b/setup/wechat/odoo/addons/wechat new file mode 120000 index 0000000000..55050baa74 --- /dev/null +++ b/setup/wechat/odoo/addons/wechat @@ -0,0 +1 @@ +../../../../wechat \ No newline at end of file diff --git a/setup/wechat/setup.py b/setup/wechat/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/wechat/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)