diff --git a/pos_alipay/README.rst b/pos_alipay/README.rst new file mode 100644 index 0000000000..f1217f8e58 --- /dev/null +++ b/pos_alipay/README.rst @@ -0,0 +1,94 @@ +.. image:: https://img.shields.io/badge/license-MIT-blue.svg + :target: https://opensource.org/licenses/MIT + :alt: License: MIT + +======================== + Alipay Payments in POS +======================== + +The module implements following payment workflows + +Barcode Payment +--------------- + +* Cashier creates order and scan user's QR in user's Alipay mobile app + + * scanning can be done via Mobile Phone camera (``pos_mobile`` module is recommended) + * scanning can be done via usb scanner + * scanning can be done via usb scanner attached to PosBox + +* User's receives order information and authorise fund transferring +* Cashier gets payment confirmation in POS + +QR Code Payment +--------------- + +* Cashier clicks a button to get one-time url and shows it to Buyer as a QR Code + + * QR can be shown in POS + * QR can be shown in Mobile POS (``pos_mobile`` module is recommended) + * QR can be shown in Customer screen + +* Buyer scans to finish the transaction. +* Cashier gets payment confirmation in POS + +Debugging +========= + +Scanning +-------- + +If you don't have camera or scanner, you can executing following code in browser console to simulate scanning:: + + odoo.__DEBUG__.services['web.core'].bus.trigger('qr_scanned', '28763443825664394'); + +Customer Screen +--------------- + +To emulate Customer screen do as following: + +* run another odoo on a different port, say ``9069``, workers 1, extra *server wide modules*, i.e. use ``--workers=1 --load=web,hw_proxy,hw_posbox_homepage,hw_screen`` +* open page at your browser: http://localhost:9069/point_of_sale/display -- you must see message ``POSBox Client display`` +* at POS' Settings activate ``[x] PosBox``, activate ``[x] Customer Display`` and set **IP Address** to ``localhost:9069`` +* Now just open POS + +Roadmap +======= + +* TODO: In sake of UX, we need to add ``alipay_order_id`` reference to ``account.bank.statement.line`` + +Credits +======= + +Contributors +------------ +* `Kolushov Alexandr `__ + +Sponsors +-------- +* `IT-Projects LLC `__ + +Maintainers +----------- +* `IT-Projects LLC `__ + + To get a guaranteed support you are kindly requested to purchase the module at `odoo apps store `__. + + Thank you for understanding! + + `IT-Projects Team `__ + +Further information +=================== + +Demo: http://runbot.it-projects.info/demo/pos_addons/11.0 + +HTML Description: https://apps.odoo.com/apps/modules/11.0/pos_payment_alipay/ + +Usage instructions: ``_ + +Changelog: ``_ + +Notifications on updates: `via Atom `_, `by Email `_ + +Tested on Odoo 11.0 ee2b9fae3519c2494f34dacf15d0a3b5bd8fbd06 diff --git a/pos_alipay/__init__.py b/pos_alipay/__init__.py new file mode 100644 index 0000000000..d7c7b9400f --- /dev/null +++ b/pos_alipay/__init__.py @@ -0,0 +1,3 @@ +# License MIT (https://opensource.org/licenses/MIT). +from . import models +from . import wizard diff --git a/pos_alipay/__manifest__.py b/pos_alipay/__manifest__.py new file mode 100644 index 0000000000..a314769256 --- /dev/null +++ b/pos_alipay/__manifest__.py @@ -0,0 +1,35 @@ +# Copyright 2018 Ivan Yelizariev +# Copyright 2018 Dinar Gabbasov +# License MIT (https://opensource.org/licenses/MIT). +{ + "name": """Alipay Payments in POS""", + "summary": """Support payment by scanning user's QR""", + "category": "Point of Sale", + # "live_test_url": "", + "images": ["images/pos_alipay.png"], + "version": "11.0.1.0.0", + "application": False, + "author": "IT-Projects LLC, Kolushov Alexandr", + "support": "apps@it-projects.info", + "website": "https://it-projects.info/team/KolushovAlexandr", + "license": "LGPL-3", + # "price": 9.00, + # "currency": "EUR", + "depends": [ + "alipay", + "pos_qr_scan", + "pos_qr_show", + "pos_qr_payments", + "pos_longpolling", + ], + "external_dependencies": {"python": [], "bin": []}, + "data": [ + "views/assets.xml", + "wizard/pos_payment_views.xml", + "security/alipay_security.xml", + ], + "demo": [], + "qweb": ["static/src/xml/pos.xml"], + "auto_install": False, + "installable": True, +} diff --git a/pos_alipay/doc/changelog.rst b/pos_alipay/doc/changelog.rst new file mode 100644 index 0000000000..9ee2b48b8e --- /dev/null +++ b/pos_alipay/doc/changelog.rst @@ -0,0 +1,4 @@ +`1.0.0` +------- + +- Init version diff --git a/pos_alipay/doc/index.rst b/pos_alipay/doc/index.rst new file mode 100644 index 0000000000..2ffc9b278e --- /dev/null +++ b/pos_alipay/doc/index.rst @@ -0,0 +1,49 @@ +======================== + Alipay Payments in POS +======================== + +Follow instructions of `Alipay API `__ module. + +Installation +============ + +* `Install `__ this module in a usual way + +Configuration +============= + +Alipay Journals +--------------- + +Alipay Journals are created automatically on first opening 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 +===== + +Scanning customer's QR +---------------------- + +* Start POS +* Create some Order +* Click ``[Scan QR Code]`` or use QR Scanner device attached to PosBox or the device you use (computer, tablet, phone) +* Ask customer to prepare QR in Alipay app +* Scan the QR +* Wait until customer authorise the payment in his Alipay app +* RESULT: Payment is proceeded. Use your Alipay Seller control panel to see balance update. + +Refunds +------- + +* Make Refund Order via backend as usual: + + * Go to ``[[ Point of Sale ]] >> Orders >> Orders`` + * Open product to be refuned + * Click button ``[Return Products]`` + +* In Refund Order click ``[Payment]`` +* In **Payment Mode** specify a Alipay journal +* Specify **Alipay Order to refund** diff --git a/pos_alipay/images/pos_alipay.png b/pos_alipay/images/pos_alipay.png new file mode 100644 index 0000000000..09f0dba8ba Binary files /dev/null and b/pos_alipay/images/pos_alipay.png differ diff --git a/pos_alipay/models/__init__.py b/pos_alipay/models/__init__.py new file mode 100644 index 0000000000..f64ec3672c --- /dev/null +++ b/pos_alipay/models/__init__.py @@ -0,0 +1,4 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from . import alipay_pos +from . import alipay_order +from . import pos_config diff --git a/pos_alipay/models/alipay_order.py b/pos_alipay/models/alipay_order.py new file mode 100644 index 0000000000..0ab090265a --- /dev/null +++ b/pos_alipay/models/alipay_order.py @@ -0,0 +1,71 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +import json + +from odoo import api, models + +from odoo.addons.qr_payments.tools import odoo_async_call + + +class AlipayOrder(models.Model): + _inherit = ["alipay.order", "alipay.pos"] + _name = "alipay.order" + + @api.multi + def _prepare_message(self): + self.ensure_one() + result_json = json.loads(self.result_raw) + msg = { + "event": "payment_result", + "code": result_json["code"], + "order_ref": self.order_ref, + "total_amount": self.total_amount, + "journal_id": self.journal_id.id, + } + return msg + + def on_notification(self, data): + order = super(AlipayOrder, self).on_notification(data) + if order and order.pos_id: + order._send_pos_notification() + return order + + @api.model + def create_qr(self, lines, **kwargs): + pos_id = kwargs.get("pos_id") + if pos_id: + if "create_vals" not in kwargs: + kwargs["create_vals"] = {} + kwargs["create_vals"]["pos_id"] = pos_id + return super(AlipayOrder, self).create_qr(lines, **kwargs) + + @api.model + def _prepare_pos_create_from_qr(self, **kwargs): + create_vals = { + "pos_id": kwargs["pos_id"], + } + kwargs.update(create_vals=create_vals) + args = () + return args, kwargs + + @api.model + def pos_create_from_qr_sync(self, **kwargs): + args, kwargs = self._prepare_pos_create_from_qr(**kwargs) + record = self._create_from_qr(*args, **kwargs) + return record._prepare_message() + + @api.model + def pos_create_from_qr(self, **kwargs): + """Async method. Result is sent via longpolling""" + args, kwargs = self._prepare_pos_create_from_qr(**kwargs) + odoo_async_call( + self._create_from_qr, + args, + kwargs, + callback=self._send_pos_notification_callback, + ) + return "ok" + + @api.model + def _send_pos_notification_callback(self, record): + record._send_pos_notification() diff --git a/pos_alipay/models/alipay_pos.py b/pos_alipay/models/alipay_pos.py new file mode 100644 index 0000000000..3676f4e8dd --- /dev/null +++ b/pos_alipay/models/alipay_pos.py @@ -0,0 +1,20 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +from odoo import api, fields, models + +CHANNEL_ALIPAY = "alipay" + + +class AlipayPos(models.AbstractModel): + _name = "alipay.pos" + + pos_id = fields.Many2one("pos.config") + + @api.multi + def _send_pos_notification(self): + self.ensure_one() + msg = self._prepare_message() + assert self.pos_id, "The record has empty value of pos_id field" + return self.env["pos.config"]._send_to_channel_by_id( + self._cr.dbname, self.pos_id.id, CHANNEL_ALIPAY, msg, + ) diff --git a/pos_alipay/models/pos_config.py b/pos_alipay/models/pos_config.py new file mode 100644 index 0000000000..176b09c11e --- /dev/null +++ b/pos_alipay/models/pos_config.py @@ -0,0 +1,119 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +from odoo import api, models + +MODULE = "pos_alipay" + + +class PosConfig(models.Model): + _inherit = "pos.config" + + @api.multi + def open_session_cb(self): + res = super(PosConfig, self).open_session_cb() + self.init_pos_alipay_journals() + return res + + def init_pos_alipay_journals(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 + alipay_journal_active = journal_obj.search( + [("company_id", "=", user.company_id.id), ("alipay", "!=", False)] + ) + if alipay_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, + } + alipay_show_journal = self._create_alipay_journal( + dict( + sequence_name="Alipay Payments by Showing QR", + prefix="ALISHOW-- ", + journal_name="Alipay Payments by Showing QR", + code="ALISHOW", + alipay="show", + **options + ) + ) + alipay_scan_journal = self._create_alipay_journal( + dict( + sequence_name="Alipay Payments by Scanning QR", + prefix="ALISCAN- ", + journal_name="Alipay Payments by Scanning QR", + code="ALISCAN", + alipay="scan", + **options + ) + ) + if demo_is_on: + self.write( + { + "journal_ids": [ + (4, alipay_show_journal.id), + (4, alipay_scan_journal.id), + ], + } + ) + + def _create_alipay_journal(self, vals): + user = self.env.user + new_sequence = self.env["ir.sequence"].create( + { + "name": vals["sequence_name"] + str(user.company_id.id), + "padding": 3, + "prefix": vals["prefix"] + str(user.company_id.id), + } + ) + self.env["ir.model.data"].create( + { + "name": "journal_sequence" + str(new_sequence.id), + "model": "ir.sequence", + "module": MODULE, + "res_id": new_sequence.id, + "noupdate": True, # If it's False, target record (res_id) will be removed while module update + } + ) + alipay_journal = self.env["account.journal"].create( + { + "name": vals["journal_name"], + "code": vals["code"], + "type": vals["type"], + "alipay": vals["alipay"], + "journal_user": True, + "sequence_id": new_sequence.id, + } + ) + self.env["ir.model.data"].create( + { + "name": "alipay_journal_" + str(alipay_journal.id), + "model": "account.journal", + "module": MODULE, + "res_id": int(alipay_journal.id), + "noupdate": True, # If it's False, target record (res_id) will be removed while module update + } + ) + if vals["write_statement"]: + self.write({"journal_ids": [(4, alipay_journal.id)]}) + current_session = self.current_session_id + statement = [ + ( + 0, + 0, + { + "name": current_session.name, + "journal_id": alipay_journal.id, + "user_id": user.id, + "company_id": user.company_id.id, + }, + ) + ] + current_session.write({"statement_ids": statement}) + return alipay_journal diff --git a/pos_alipay/security/alipay_security.xml b/pos_alipay/security/alipay_security.xml new file mode 100644 index 0000000000..7ac15e2530 --- /dev/null +++ b/pos_alipay/security/alipay_security.xml @@ -0,0 +1,20 @@ + + + + + + Mini-Program: mini-program user: read POS products only + + [('sale_ok', '=', True), ('available_in_pos', '=', True)] + + + + + + + diff --git a/pos_alipay/static/description/icon.png b/pos_alipay/static/description/icon.png new file mode 100644 index 0000000000..8a058284ed Binary files /dev/null and b/pos_alipay/static/description/icon.png differ diff --git a/pos_alipay/static/description/index.html b/pos_alipay/static/description/index.html new file mode 100644 index 0000000000..63740febae --- /dev/null +++ b/pos_alipay/static/description/index.html @@ -0,0 +1,100 @@ +
+
+
+

Alipay Payments in POS

+

Connecting tool for Alipay and POS in Odoo

+
+
+
+ +
+
+
+
+ If you don't know how Asia turned down cash with Alipay, check, for example, this video. +
+
+
+
+ +
+
+

Show QR code to buyer

+
+ +
+
+
+ + +
+
+

Scan buyer's QR code via device's camera

+
+ +
+
+
+ +
+
+

Scan buyer's QR code via external device

+
+ +
+
+
+ + +
+
+
+

Need our service?

+

Contact us by email or fill out request form

+ +
+
+
+
+ Tested on Odoo
11.0 community +
+
+ Tested on Odoo
11.0 enterprise +
+
+
+
+
diff --git a/pos_alipay/static/description/pos_1.png b/pos_alipay/static/description/pos_1.png new file mode 100644 index 0000000000..401cb0cab7 Binary files /dev/null and b/pos_alipay/static/description/pos_1.png differ diff --git a/pos_alipay/static/description/pos_2.png b/pos_alipay/static/description/pos_2.png new file mode 100644 index 0000000000..553b411a5e Binary files /dev/null and b/pos_alipay/static/description/pos_2.png differ diff --git a/pos_alipay/static/description/pos_3.png b/pos_alipay/static/description/pos_3.png new file mode 100644 index 0000000000..9c9cd10abb Binary files /dev/null and b/pos_alipay/static/description/pos_3.png differ diff --git a/pos_alipay/static/src/js/alipay.js b/pos_alipay/static/src/js/alipay.js new file mode 100644 index 0000000000..8f09c0c088 --- /dev/null +++ b/pos_alipay/static/src/js/alipay.js @@ -0,0 +1,194 @@ +/* Copyright 2018 Ivan Yelizariev + License MIT (https://opensource.org/licenses/MIT). */ +odoo.define("pos_alipay", function(require) { + "use strict"; + + require("pos_qr_scan"); + require("pos_qr_show"); + var rpc = require("web.rpc"); + var core = require("web.core"); + var models = require("point_of_sale.models"); + + models.load_fields("account.journal", ["alipay"]); + + var exports = {}; + + var PosModelSuper = models.PosModel; + models.PosModel = models.PosModel.extend({ + initialize: function() { + var self = this; + PosModelSuper.prototype.initialize.apply(this, arguments); + this.alipay = new exports.Alipay(this); + + this.bus.add_channel_callback("alipay", this.on_alipay, this); + this.ready.then(function() { + // Take out alipay scan cashregister from cashregisters to avoid + // rendering in payment screent + self.scan_journal = self.hide_cashregister(function(r) { + return r.alipay === "scan"; + }); + }); + }, + scan_product: function(parsed_code) { + // TODO: do we need to make this optional? + var value = parsed_code.code; + if (this.alipay.check_auth_code(value)) { + this.alipay.process_qr(value); + return true; + } + return PosModelSuper.prototype.scan_product.apply(this, arguments); + }, + on_alipay: function(msg) { + this.add_qr_payment( + msg.order_ref, + msg.journal_id, + msg.total_fee / 100.0, + { + scan_id: msg.scan_id, + }, + // Auto validate payment + true + ); + }, + alipay_qr_payment: function(order, creg) { + /* Send request asynchronously */ + var self = this; + + var pos = this; + var terminal_ref = "POS/" + pos.config.name; + var pos_id = pos.config.id; + + var lines = order.orderlines.map(function(r) { + return { + // Always use 1 because quantity is taken into account in price field + quantity: 1, + quantity_full: r.get_quantity(), + price: r.get_price_with_tax(), + product_id: r.get_product().id, + }; + }); + + // Send without repeating on failure + return rpc + .query({ + model: "alipay.order", + method: "create_qr", + kwargs: { + lines: lines, + subject: order.name, + order_ref: order.uid, + pay_amount: order.get_due(), + terminal_ref: terminal_ref, + pos_id: pos_id, + journal_id: creg.journal.id, + }, + }) + .then(function(data) { + if (data.code_url) { + self.on_payment_qr(order, data.code_url); + } else if (data.error) { + self.show_warning(data.error); + } else { + self.show_warning("Unknown error"); + } + }); + }, + }); + + var OrderSuper = models.Order; + models.Order = models.Order.extend({ + add_paymentline: function(cashregister) { + if (cashregister.journal.alipay === "show") { + this.pos.alipay_qr_payment(this, cashregister); + return; + } + return OrderSuper.prototype.add_paymentline.apply(this, arguments); + }, + }); + + var PaymentlineSuper = models.Paymentline; + models.Paymentline = models.Paymentline.extend({ + initialize: function(attributes, options) { + PaymentlineSuper.prototype.initialize.apply(this, arguments); + this.scan_id = options.scan_id; + }, + // TODO: do we need to extend init_from_JSON too ? + export_as_JSON: function() { + var res = PaymentlineSuper.prototype.export_as_JSON.apply(this, arguments); + res.scan_id = this.scan_id; + return res; + }, + }); + + exports.Alipay = window.Backbone.Model.extend({ + initialize: function(pos) { + var self = this; + this.pos = pos; + core.bus.on("qr_scanned", this, function(value) { + if (self.check_auth_code(value)) { + self.process_qr(value); + } + }); + }, + check_auth_code: function(code) { + // TODO: do we need to integrate this with barcode.nomenclature? + var beginning = code.substring(0, 2); + if ( + code && + Number.isInteger(Number(code)) && + code.length >= 16 && + code.length <= 24 && + Number(beginning) >= 25 && + Number(beginning) <= 30 + ) { + return true; + } + return false; + }, + process_qr: function(auth_code) { + var order = this.pos.get_order(); + if (!order) { + return; + } + // TODO: block order for editing + this.scan(auth_code, order); + }, + scan: function(auth_code, order) { + /* Send request asynchronously */ + var self = this; + + var terminal_ref = "POS/" + self.pos.config.name; + var pos_id = self.pos.config.id; + + var send_it = function() { + return rpc.query({ + model: "alipay.order", + method: "pos_create_from_qr", + kwargs: { + auth_code: auth_code, + total_amount: order.get_due(), + order_ref: order.uid, + subject: order.name, + terminal_ref: terminal_ref, + journal_id: self.pos.scan_journal.id, + pos_id: pos_id, + }, + }); + }; + + var current_send_number = 0; + return send_it().fail(function(error, e) { + if (self.pos.debug) { + console.log( + "Alipay", + self.pos.config.name, + "failed request #" + current_send_number + ":", + error.message + ); + } + self.pos.show_warning(); + }); + }, + }); + return exports; +}); diff --git a/pos_alipay/static/src/js/tour.js b/pos_alipay/static/src/js/tour.js new file mode 100644 index 0000000000..65e800a01b --- /dev/null +++ b/pos_alipay/static/src/js/tour.js @@ -0,0 +1,86 @@ +/* - Copyright 2018 Ivan Yelizariev + License MIT (https://opensource.org/licenses/MIT). */ +/* This file is not used until we make a CI tool, that can run it. Normal CI cannot use longpolling. + See https://github.com/odoo/odoo/commit/673f4aa4a77161dc58e0e1bf97e8f713b1e88491 + */ +odoo.define("pos_alipay.tour", function(require) { + "use strict"; + + var DUMMY_AUTH_CODE = "134579302432164181"; + var tour = require("web_tour.tour"); + var core = require("web.core"); + var _t = core._t; + + function open_pos_neworder() { + return [ + { + trigger: + '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"], .oe_menu_toggler[data-menu-xmlid="point_of_sale.menu_point_root"]', + content: _t( + "Ready to launch your point of sale? Click here." + ), + position: "bottom", + }, + { + trigger: ".o_pos_kanban button.oe_kanban_action_button", + content: _t( + "

Click to start the point of sale interface. It runs on tablets, laptops, or industrial hardware.

Once the session launched, the system continues to run without an internet connection.

" + ), + position: "bottom", + }, + { + content: "Switch to table or make dummy action", + trigger: + ".table:not(.oe_invisible .neworder-button), .order-button.selected", + position: "bottom", + }, + { + content: "waiting for loading to finish", + trigger: ".order-button.neworder-button", + }, + ]; + } + + function add_product_to_order(product_name) { + return [ + { + content: "buy " + product_name, + trigger: '.product-list .product-name:contains("' + product_name + '")', + }, + { + content: "the " + product_name + " have been added to the order", + trigger: '.order .product-name:contains("' + product_name + '")', + }, + ]; + } + + var steps = []; + steps = steps.concat(open_pos_neworder()); + steps = steps.concat(add_product_to_order("Miscellaneous")); + // Simulate qr scanning + steps = steps.concat([ + { + content: "Make dummy action and trigger scanning event", + trigger: ".order-button.selected", + run: function() { + core.bus.trigger("qr_scanned", DUMMY_AUTH_CODE); + }, + }, + ]); + // Wait until order is proceeded + steps = steps.concat([ + { + content: "Screen is changed to payment screen", + trigger: ".button_next", + run: function() { + // No need to click on the button + }, + }, + { + content: + "Screen is changed to receipt or products screen (depends on settings)", + trigger: ".button_print,.order-button", + }, + ]); + tour.register("tour_pos_debt_notebook", {test: true, url: "/web"}, steps); +}); diff --git a/pos_alipay/static/src/xml/pos.xml b/pos_alipay/static/src/xml/pos.xml new file mode 100644 index 0000000000..64d7c8e355 --- /dev/null +++ b/pos_alipay/static/src/xml/pos.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/pos_alipay/tests/__init__.py b/pos_alipay/tests/__init__.py new file mode 100644 index 0000000000..f093a716a2 --- /dev/null +++ b/pos_alipay/tests/__init__.py @@ -0,0 +1,2 @@ +# License MIT (https://opensource.org/licenses/MIT). +from . import test_alipay diff --git a/pos_alipay/tests/test_alipay.py b/pos_alipay/tests/test_alipay.py new file mode 100644 index 0000000000..d35a95cfd6 --- /dev/null +++ b/pos_alipay/tests/test_alipay.py @@ -0,0 +1,157 @@ +# Copyright 2018 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +import logging + +from odoo.addons.alipay.tests.test_alipay import DUMMY_RSA_KEY +from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon + +_logger = logging.getLogger(__name__) +DUMMY_AUTH_CODE = "134579302432164181" +DUMMY_POS_ID = 1 + + +class TestAlipayOrder(TestPointOfSaleCommon): + at_install = True + post_install = True + + def setUp(self): + super(TestAlipayOrder, self).setUp() + self.env["ir.config_parameter"].set_param("alipay.local_sandbox", "1") + + # create alipay journals + self.pos_config.init_pos_alipay_journals() + context = dict(app_private_key_string=DUMMY_RSA_KEY,) + + self.Config = self.env["ir.config_parameter"].with_context(context) + self.Order = self.env["alipay.order"].with_context(context) + self.Refund = self.env["alipay.refund"].with_context(context) + self.PosMakePayment = self.PosMakePayment.with_context(context) + self.product1 = self.env["product.product"].create({"name": "Product1"}) + self.product2 = self.env["product.product"].create({"name": "Product2"}) + + def _create_pos_order(self): + # I create a new PoS order with 2 lines + order = self.PosOrder.create( + { + "company_id": self.company_id, + "partner_id": self.partner1.id, + "pricelist_id": self.partner1.property_product_pricelist.id, + "lines": [ + ( + 0, + 0, + { + "name": "OL/0001", + "product_id": self.product3.id, + "price_unit": 450, + "discount": 5.0, + "qty": 2.0, + "tax_ids": [(6, 0, self.product3.taxes_id.ids)], + }, + ), + ( + 0, + 0, + { + "name": "OL/0002", + "product_id": self.product4.id, + "price_unit": 300, + "discount": 5.0, + "qty": 3.0, + "tax_ids": [(6, 0, self.product4.taxes_id.ids)], + }, + ), + ], + } + ) + return order + + def _create_alipay_order(self): + self.lines = [ + { + "product_id": self.product1.id, + "name": "Product 1 Name", + "quantity": 2, + "price": 4.50, + "category": "123456", + "description": "翻译服务器错误", + }, + { + "product_id": self.product2.id, + "name": "Product 2 Name", + "quantity": 3, + "price": 3.0, + "category": "123456", + "description": "網路白目哈哈", + }, + ] + order, code_url = self.Order._create_qr( + self.lines, "Test Order", total_amount=3.0 + ) + self.assertEqual(order.state, "draft", "Just created order has wrong state") + return order + + def test_refund(self): + # Order are not really equal because I'm lazy + # Just imagine that they are correspond each other + order = self._create_pos_order() + alipay_order = self._create_alipay_order() + order.alipay_order_id = alipay_order.id + + # I create a refund + refund_action = order.refund() + refund = self.PosOrder.browse(refund_action["res_id"]) + + alipay_journal = self.env["account.journal"].search([("alipay", "=", "show")]) + + payment_context = {"active_ids": refund.ids, "active_id": refund.id} + refund_payment = self.PosMakePayment.with_context(**payment_context).create( + { + "amount": refund.amount_total, + "journal_id": alipay_journal.id, + "alipay_order_id": alipay_order.id, + } + ) + + # I click on the validate button to register the payment. + refund_payment.with_context(**payment_context).check() + + self.assertEqual(refund.state, "paid", "The refund is not marked as paid") + + self.assertEqual( + alipay_order.state, + "refunded", + "Alipay Order state is not changed after making refund payment", + ) + + def test_scan(self): + """Test payment workflow from server side. + + * Cashier scanned buyer's QR and upload it to odoo server, + odoo server sends information to alipay servers and wait for response with result. + + * Once user authorize the payment, odoo receives result syncroniosly from + previously sent request. + + * Odoo sends result to POS via longpolling. + + Due to limititation of testing framework, we use syncronios call for testing + + """ + + journal = self.env["account.journal"].search([("alipay", "=", "scan")]) + + # make request with scanned qr code (auth_code) + msg = self.env["alipay.order"].pos_create_from_qr_sync( + **{ + "auth_code": DUMMY_AUTH_CODE, + "terminal_ref": "POS/%s" % DUMMY_POS_ID, + "pos_id": DUMMY_POS_ID, + "journal_id": journal.id, + "total_amount": 1, + "subject": "Order #1", + } + ) + self.assertEqual( + msg.get("code"), "10003", "Wrong result code. The patch doesn't work?" + ) diff --git a/pos_alipay/views/assets.xml b/pos_alipay/views/assets.xml new file mode 100644 index 0000000000..95bf8e2ada --- /dev/null +++ b/pos_alipay/views/assets.xml @@ -0,0 +1,11 @@ + + + + +