[IMP] ADD MOBULE MOBILE WEB
This commit is contained in:
parent
3f495222a4
commit
47a8cb9d4c
|
|
@ -1,128 +0,0 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
parts/
|
||||
sdist/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# github action
|
||||
.github/workflows/*yaml
|
||||
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Barcode in Mobile',
|
||||
'category': 'Hidden',
|
||||
'author': 'Expert Co. Ltd.',
|
||||
'website': 'http://www.exp-sa.com',
|
||||
'summary': 'Barcode scan in Mobile',
|
||||
'version': '1.0',
|
||||
'description': """ """,
|
||||
'depends': ['barcodes', 'odex25_web_mobile'],
|
||||
'data': ['views/odex25_barcodes_mobile_template.xml'],
|
||||
'qweb': ['static/src/xml/barcodes.xml'],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -0,0 +1,33 @@
|
|||
odoo.define('odex25_barcodes_mobile.BarcodeEvents', function (require) {
|
||||
"use strict";
|
||||
|
||||
var BarcodeEvents = require('barcodes.BarcodeEvents');
|
||||
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
|
||||
if (!mobile.methods.closeVirtualKeyboard) {
|
||||
return;
|
||||
}
|
||||
var barcodeEvents = BarcodeEvents.BarcodeEvents;
|
||||
|
||||
// Each time the input has the focus, the mobile virtual keyboard will
|
||||
// be opened but we don't control exactly when.
|
||||
// In some are cases, the opening is slowly deferred. The keyboard
|
||||
// will appear anyway and closeVirtualKeyboard will be executed too early.
|
||||
barcodeEvents.$barcodeInput.on('focus', function () {
|
||||
setTimeout(mobile.methods.closeVirtualKeyboard, 0);
|
||||
});
|
||||
|
||||
// On mobile app, we can keep the input focused as the virtual keyboard
|
||||
// is closed by a specific method.
|
||||
barcodeEvents._blurBarcodeInput = function () {
|
||||
if (this.$barcodeInput) {
|
||||
this.$barcodeInput.val('');
|
||||
}
|
||||
}
|
||||
barcodeEvents.__blurBarcodeInput = _.debounce(barcodeEvents._blurBarcodeInput,
|
||||
barcodeEvents.inputTimeOut);
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
odoo.define('odex25_web_mobile.barcode_mobile_mixin', function (require) {
|
||||
"use strict";
|
||||
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
return {
|
||||
events: {
|
||||
'click .o_mobile_barcode': 'open_mobile_scanner'
|
||||
},
|
||||
async start() {
|
||||
const res = await this._super(...arguments);
|
||||
if (!mobile.methods.scanBarcode) {
|
||||
this.$el.find(".o_mobile_barcode").remove();
|
||||
}
|
||||
return res;
|
||||
},
|
||||
async open_mobile_scanner() {
|
||||
const response = await mobile.methods.scanBarcode();
|
||||
const barcode = response.data;
|
||||
if (barcode) {
|
||||
this._onBarcodeScanned(barcode);
|
||||
mobile.methods.vibrate({'duration': 100});
|
||||
} else {
|
||||
mobile.methods.showToast({'message': 'Please, Scan again !!'});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
.o_barcode_mobile_container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 1.08333333rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #fff;
|
||||
}
|
||||
.o_barcode_mobile_container img, .o_barcode_mobile_container .o_mobile_barcode {
|
||||
width: 115px;
|
||||
height: 60px;
|
||||
}
|
||||
.o_barcode_mobile_container .o_mobile_barcode {
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
opacity: 0.75;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.o_barcode_mobile_container .o_mobile_barcode .o_barcode_mobile_camera {
|
||||
margin: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
.o_barcode_mobile_container .o_mobile_barcode div {
|
||||
font-size: 12px;
|
||||
}
|
||||
.o_barcode_mobile_container .o_barcode_laser {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -15px;
|
||||
bottom: auto;
|
||||
right: -15px;
|
||||
height: 5px;
|
||||
background: rgba(255, 0, 0, 0.6);
|
||||
box-shadow: 0 1px 10px 1px rgba(255, 0, 0, 0.8);
|
||||
animation: o_barcode_scanner_intro 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) 0.4s;
|
||||
}
|
||||
@keyframes o_barcode_scanner_intro {
|
||||
25% {
|
||||
top: 75%;
|
||||
}
|
||||
50% {
|
||||
top: 0;
|
||||
}
|
||||
75% {
|
||||
top: 100%;
|
||||
}
|
||||
100% {
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 576px) {
|
||||
.o_event_barcode_bg.o_home_menu_background {
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
}/*# sourceMappingURL=barcode_mobile.css.map */
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"sources":["barcode_mobile.scss","barcode_mobile.css"],"names":[],"mappings":"AAAA;EACI,kBAAA;EACA,qBAAA;EACA,+BAAA;EACA,wBAAA;EACA,gBAAA;EACA,gBAAA;EACA,WAAA;ACCJ;ADCI;EACI,YAAA;EACA,YAAA;ACCR;ADEI;EACI,WAAA;EACA,SAAA;EACA,kBAAA;EACA,aAAA;EACA,gBAAA;ACAR;ADEQ;EACI,WAAA;EACA,cAAA;ACAZ;ADGQ;EACI,eAAA;ACDZ;ADKI;EAEI,kBAAA;EACA,QAAA;EACA,WAAA;EACA,YAAA;EACA,YAAA;EAEA,WAAA;EACA,gCAAA;EACA,+CAAA;EACA,iFAAA;ACLR;ADQI;EACI;IACI,QAAA;ECNV;EDQM;IACI,MAAA;ECNV;EDQM;IACI,SAAA;ECNV;EDQM;IACI,QAAA;ECNV;AACF;;ADSA;EACI;IACI,iBAAA;IACA,YAAA;ECNN;AACF","file":"barcode_mobile.css"}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
.o_barcode_mobile_container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-size: 1.08333333rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #fff;
|
||||
|
||||
img, .o_mobile_barcode {
|
||||
width: 115px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.o_mobile_barcode {
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
opacity: 0.75;
|
||||
padding-top: 5px;
|
||||
|
||||
.o_barcode_mobile_camera {
|
||||
margin: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_barcode_laser {
|
||||
// avoid dependencies to web file in pos
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -15px;
|
||||
bottom: auto;
|
||||
right: -15px;
|
||||
|
||||
height: 5px;
|
||||
background: rgba(red, 0.6);
|
||||
box-shadow: 0 1px 10px 1px rgba(red, 0.8);
|
||||
animation: o_barcode_scanner_intro 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) 0.4s;
|
||||
}
|
||||
|
||||
@keyframes o_barcode_scanner_intro {
|
||||
25% {
|
||||
top: 75%;
|
||||
}
|
||||
50% {
|
||||
top: 0;
|
||||
}
|
||||
75% {
|
||||
top: 100%;
|
||||
}
|
||||
100% {
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 576px) {
|
||||
.o_event_barcode_bg.o_home_menu_background {
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mobile_barcode_template">
|
||||
<div class="o_barcode_mobile_container">
|
||||
<a role="button" class="btn btn-primary o_mobile_barcode">
|
||||
<i class="fa fa-camera fa-2x o_barcode_mobile_camera"/>
|
||||
Tap to scan
|
||||
</a>
|
||||
<img src="/barcodes/static/img/barcode.png" alt="Barcode"/>
|
||||
<span class="o_barcode_laser"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="assets_backend" name="odex25_barcodes_mobile assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/odex25_barcodes_mobile/static/src/js/barcode_events.js"></script>
|
||||
<script type="text/javascript" src="/odex25_barcodes_mobile/static/src/js/barcode_mobile_mixin.js"></script>
|
||||
<link rel="stylesheet" type="text/scss" href="/odex25_barcodes_mobile/static/src/scss/barcode_mobile.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Event Barcode in Mobile',
|
||||
'category': 'Odex25 Marketing/Events',
|
||||
'author': "Expert Co. Ltd.",
|
||||
'website': "http://www.exp-sa.com",
|
||||
'summary': 'Event Barcode scan in Mobile',
|
||||
'version': '1.0',
|
||||
'description': """ """,
|
||||
'depends': ['odex25_event_barcode', 'odex25_barcodes_mobile'],
|
||||
'qweb': ['static/src/xml/odex25_event_barcode_mobile.xml'],
|
||||
'data': ['views/odex25_event_barcode_mobile_template.xml'],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * odex25_event_barcode_mobile
|
||||
#
|
||||
# Translators:
|
||||
# Mustafa Rawi <mustafa@cubexco.com>, 2019
|
||||
# Martin Trigaux, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~11.5+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-09-18 10:05+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:35+0000\n"
|
||||
"Last-Translator: Martin Trigaux, 2019\n"
|
||||
"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: ar\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||
|
||||
#. module: odex25_event_barcode_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_event_barcode_mobile/static/src/xml/odex25_event_barcode_mobile.xml:8
|
||||
#, python-format
|
||||
msgid "Barcode"
|
||||
msgstr "باركود"
|
||||
|
||||
#. module: odex25_event_barcode_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_event_barcode_mobile/static/src/xml/odex25_event_barcode_mobile.xml:7
|
||||
#, python-format
|
||||
msgid "Tap to scan"
|
||||
msgstr "انقر للقراءة"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -0,0 +1,10 @@
|
|||
odoo.define('web.event.barcode_mobile', function (require) {
|
||||
"use strict";
|
||||
|
||||
const EventBarcodeScanView = require('odex25_event_barcode.EventScanView');
|
||||
const barcodeMobileMixin = require('odex25_web_mobile.barcode_mobile_mixin');
|
||||
|
||||
EventBarcodeScanView.include(Object.assign({}, barcodeMobileMixin, {
|
||||
events: Object.assign({}, barcodeMobileMixin.events, EventBarcodeScanView.prototype.events)
|
||||
}));
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="odex25_event_barcode_template">
|
||||
<t t-jquery=".o_odex25_event_barcode_image" t-operation="replace">
|
||||
<t t-call="mobile_barcode_template"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" inherit_id="web.assets_backend" name="Event Barcode Mobile Assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/odex25_event_barcode_mobile/static/src/js/odex25_event_barcode_mobile.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
{
|
||||
'name': 'Mobile',
|
||||
'category': 'Odex25 Hidden',
|
||||
'author': 'Expert Co. Ltd.',
|
||||
'website': 'http://www.exp-sa.com',
|
||||
'summary': 'Odoo Mobile Core module',
|
||||
'version': '1.0',
|
||||
'description': """
|
||||
This module provides the core of the Odoo Mobile App.
|
||||
""",
|
||||
'depends': [
|
||||
'odex25_web',
|
||||
],
|
||||
'qweb': ['static/src/xml/*.xml'],
|
||||
'data': [
|
||||
'views/mobile_template.xml',
|
||||
'views/views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
#'auto_install': True,
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * odex25_web_mobile
|
||||
#
|
||||
# Translators:
|
||||
# Mustafa Rawi <mustafa@cubexco.com>, 2020
|
||||
# amrnegm <amrnegm.01@gmail.com>, 2020
|
||||
# Osama Ahmaro <osamaahmaro@gmail.com>, 2020
|
||||
# Zuhair Hammadi <zuhair12@gmail.com>, 2020
|
||||
# Shaima Safar <shaima.safar@open-inside.com>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-01 07:40+0000\n"
|
||||
"PO-Revision-Date: 2020-09-07 08:25+0000\n"
|
||||
"Last-Translator: Shaima Safar <shaima.safar@open-inside.com>, 2020\n"
|
||||
"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: ar\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/contact_sync.xml:0
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/contact_sync.xml:0
|
||||
#, python-format
|
||||
msgid "Add"
|
||||
msgstr "إضافة"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/contact_sync.xml:0
|
||||
#, python-format
|
||||
msgid "Add to"
|
||||
msgstr "إضافة إلى"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/user_menu.xml:0
|
||||
#, python-format
|
||||
msgid "Add to Home Screen"
|
||||
msgstr "إضافته للقائمة الرئيسية"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#: model:ir.model,name:odex25_web_mobile.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr "جهة الاتصال"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#: model:ir.model.fields,field_description:odex25_web_mobile.field_res_partner__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "الاسم المعروض"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#: model:ir.model.fields,field_description:odex25_web_mobile.field_res_partner__id
|
||||
msgid "ID"
|
||||
msgstr "المُعرف"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#: model:ir.model.fields,field_description:odex25_web_mobile.field_res_partner____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "آخر تعديل في"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#: model:ir.model.fields,field_description:odex25_web_mobile.field_res_partner__image_medium
|
||||
#: model:ir.model.fields,field_description:odex25_web_mobile.field_res_users__image_medium
|
||||
msgid "Medium-sized image"
|
||||
msgstr "صورة متوسطة الحجم"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/contact_sync.xml:0
|
||||
#, python-format
|
||||
msgid "Mobile"
|
||||
msgstr "الهاتف المحمول"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/js/user_menu.js:0
|
||||
#, python-format
|
||||
msgid "No shortcut for Home Menu"
|
||||
msgstr "لا توجد اختصارات للقائمة الرئيسية"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/settings_dashboard.xml:0
|
||||
#, python-format
|
||||
msgid "On Apple Store"
|
||||
msgstr "على متجر Apple"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/settings_dashboard.xml:0
|
||||
#, python-format
|
||||
msgid "On Google Play"
|
||||
msgstr "على متجر جوجل بلاي"
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/barcode_fields.xml:0
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/barcode_fields.xml:0
|
||||
#, python-format
|
||||
msgid "Scan barcode"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_web_mobile
|
||||
#. openerp-web
|
||||
#: code:addons/odex25_web_mobile/static/src/xml/user_menu.xml:0
|
||||
#, python-format
|
||||
msgid "Switch/Add Account"
|
||||
msgstr "تغيير الحساب/إضافة حساب جديد"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from . import res_partner
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
from odoo import fields, models
|
||||
|
||||
class Partners(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
# related for backward compatibility with < 13.0
|
||||
image_medium = fields.Binary(string="Medium-sized image", related='image_128', store=False, readonly=True)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<metadata id="metadata27">image/svg+xml</metadata>
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<g id="g20">
|
||||
<g id="g12">
|
||||
<rect height="186" id="rect2" width="44" x="61" y="57"/>
|
||||
<rect height="186" id="rect4" width="44" x="236.5" y="57"/>
|
||||
<rect height="186" id="rect6" width="44" x="295" y="57"/>
|
||||
<rect height="186" id="rect8" width="24" x="129.5" y="57"/>
|
||||
<rect height="186" id="rect10" width="14" x="193" y="57"/>
|
||||
</g>
|
||||
<g id="g18">
|
||||
<polygon id="polygon14" points="59.5,269.5 25.5,269.5 25.5,30.5 59.5,30.5 59.5,19.5 14.5,19.5 14.5,280.5 59.5,280.5 "/>
|
||||
<polygon id="polygon16" points="339.4999694824219,19.5 339.4999694824219,30.5 374.4999694824219,30.5 374.4999694824219,269.5 339.4999694824219,269.5 339.4999694824219,280.5 385.4999694824219,280.5 385.4999694824219,19.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,29 @@
|
|||
odoo.define('odex25_web_mobile.ActionManager', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ActionManager = require('web.ActionManager');
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
/*
|
||||
We don't want to open website urls in the Odoo apps (iOS and Android).
|
||||
The apps detect the redirection and open the url in a seprate browser.
|
||||
|
||||
In Odoo desktop, the redirection occurs in the same tab and the returned
|
||||
promise is never resolved.
|
||||
This override returns a resolved promise in case of mobile app redirects
|
||||
because Odoo is not aware of this and we need to reactivate status button.
|
||||
|
||||
This behavior is the same as the one already done when opening the url in a new window.
|
||||
*/
|
||||
ActionManager.include({
|
||||
ir_actions_act_url: function (action) {
|
||||
var url = action.url;
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (!_.isEmpty(mobile.methods) && !url.startsWith("/web")) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
odoo.define('odex25_web_mobile.barcode_fields', function (require) {
|
||||
"use strict";
|
||||
|
||||
var field_registry = require('web.field_registry');
|
||||
require('web._field_registry');
|
||||
var relational_fields = require('web.relational_fields');
|
||||
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
/**
|
||||
* Override the Many2One to open a dialog in mobile.
|
||||
*/
|
||||
var FieldMany2OneBarcode = relational_fields.FieldMany2One.extend({
|
||||
template: "FieldMany2OneBarcode",
|
||||
events: _.extend({}, relational_fields.FieldMany2One.prototype.events, {
|
||||
'click .o_barcode_mobile': '_onBarcodeButtonClick',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var result = this._super.apply(this, arguments);
|
||||
this._startBarcode();
|
||||
return result;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* External button is visible
|
||||
*
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
_isExternalButtonVisible: function () {
|
||||
return this.$external_button.is(':visible');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderEdit: function () {
|
||||
this._super.apply(this, arguments);
|
||||
// Hide button if a record is set or external button is visible
|
||||
if (this.$barcode_button) {
|
||||
this.$barcode_button.toggle(!this._isExternalButtonVisible());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Initialisation of barcode button
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_startBarcode: function () {
|
||||
this.$barcode_button = this.$('.o_barcode_mobile');
|
||||
// Hide button if a record is set
|
||||
this.$barcode_button.toggle(!this.isSet());
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On click on button
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onBarcodeButtonClick: function () {
|
||||
var self = this;
|
||||
mobile.methods.scanBarcode().then(function (response){
|
||||
var barcode = response.data;
|
||||
if (barcode) {
|
||||
self._onBarcodeScanned(barcode);
|
||||
mobile.methods.vibrate({
|
||||
duration: 100,
|
||||
});
|
||||
} else {
|
||||
mobile.methods.showToast({
|
||||
message: 'Please, scan again !!',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* When barcode is scanned
|
||||
*
|
||||
* @param barcode
|
||||
* @private
|
||||
*/
|
||||
_onBarcodeScanned: function (barcode) {
|
||||
this._search(barcode);
|
||||
},
|
||||
});
|
||||
|
||||
if (mobile.methods.scanBarcode) {
|
||||
field_registry.add('many2one_barcode', FieldMany2OneBarcode);
|
||||
}
|
||||
|
||||
return FieldMany2OneBarcode;
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
odoo.define('odex25_web_mobile.ContactSync', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
var ContactSync = Widget.extend({
|
||||
template: 'ContactSync',
|
||||
events: {
|
||||
'click': '_onClick',
|
||||
},
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, params) {
|
||||
this.res_id = params.res_id;
|
||||
this.res_model = params.res_model;
|
||||
this.is_mobile = mobile.methods.addContact;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onClick: function () {
|
||||
var fieldNames = [
|
||||
'name', 'image_1920', 'parent_id', 'phone', 'mobile', 'email',
|
||||
'street', 'street2', 'city', 'state_id', 'zip', 'country_id',
|
||||
'website', 'function',
|
||||
];
|
||||
this._rpc({
|
||||
model: this.res_model,
|
||||
method: 'read',
|
||||
args: [this.res_id, fieldNames],
|
||||
}).then(function (r) {
|
||||
const contact = Object.assign({}, r[0], {
|
||||
image: r[0].image_1920,
|
||||
});
|
||||
delete contact.image_1920;
|
||||
mobile.methods.addContact(contact);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return ContactSync;
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
odoo.define('odex25_web_mobile.Dialog', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Dialog = require('web.Dialog');
|
||||
var mobileMixins = require('odex25_web_mobile.mixins');
|
||||
|
||||
Dialog.include(_.extend({}, mobileMixins.BackButtonEventMixin, {
|
||||
/**
|
||||
* Ensure that the on_attach_callback is called after the Dialog has been
|
||||
* attached to the DOM and opened.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this._opened = this._opened.then(this.on_attach_callback.bind(this));
|
||||
},
|
||||
/**
|
||||
* As the Dialog is based on Bootstrap's Modal we don't handle ourself when
|
||||
* the modal is detached from the DOM and we have to rely on their events
|
||||
* to call on_detach_callback.
|
||||
* The 'hidden.bs.modal' is triggered when the hidding animation (if any)
|
||||
* is finished and the modal is detached from the DOM.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.$modal.on('hidden.bs.modal', self.on_detach_callback.bind(self));
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Close the current dialog on 'backbutton' event.
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onBackButton: function () {
|
||||
this.close();
|
||||
},
|
||||
}));
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
odoo.define('odex25_web_mobile.mixins', function (require) {
|
||||
"use strict";
|
||||
|
||||
const session = require('web.session');
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
/**
|
||||
* Mixin to setup lifecycle methods and allow to use 'backbutton' events sent
|
||||
* from the native application.
|
||||
*
|
||||
* @mixin
|
||||
* @name BackButtonEventMixin
|
||||
*
|
||||
*/
|
||||
var BackButtonEventMixin = {
|
||||
/**
|
||||
* Register event listener for 'backbutton' event when attached to the DOM
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
mobile.backButtonManager.addListener(this, this._onBackButton);
|
||||
},
|
||||
/**
|
||||
* Unregister event listener for 'backbutton' event when detached from the DOM
|
||||
*/
|
||||
on_detach_callback: function () {
|
||||
mobile.backButtonManager.removeListener(this, this._onBackButton);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev 'backbutton' type event
|
||||
*/
|
||||
_onBackButton: function () {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mixin to hook into the controller record's saving method and
|
||||
* trigger the update of the user's account details on the mobile app.
|
||||
*
|
||||
* @mixin
|
||||
* @name UpdateDeviceAccountControllerMixin
|
||||
*
|
||||
*/
|
||||
const UpdateDeviceAccountControllerMixin = {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async saveRecord() {
|
||||
const changedFields = await this._super(...arguments);
|
||||
this.savingDef = this.savingDef.then(() => session.updateAccountOnMobileDevice());
|
||||
return changedFields;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger the update of the user's account details on the mobile app as soon as
|
||||
* the session is correctly initialized.
|
||||
*/
|
||||
session.is_bound.then(() => session.updateAccountOnMobileDevice());
|
||||
|
||||
return {
|
||||
BackButtonEventMixin: BackButtonEventMixin,
|
||||
UpdateDeviceAccountControllerMixin,
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
odoo.define('odex25_web_mobile.hooks', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { backButtonManager } = require('odex25_web_mobile.core');
|
||||
|
||||
const { Component } = owl;
|
||||
const { onWillUnmount } = owl.hooks;
|
||||
|
||||
/**
|
||||
* This hook provides support for executing code when the back button is pressed
|
||||
* on the mobile application of Odoo. This actually replaces the default back
|
||||
* button behavior so this feature should only be enabled when it is actually
|
||||
* useful.
|
||||
*
|
||||
* The feature must be enabled manually after every (re)mount (or whenever it is
|
||||
* appropriate), it can be disabled manually, and it is automatically disabled
|
||||
* on unmount and on destroy.
|
||||
*
|
||||
* @param {function} func the function to execute when the back button is
|
||||
* pressed. The function is called with the custom event as param.
|
||||
* @returns {Object} exports the enable and disable functions to allow
|
||||
* controlling the state of the listeners.
|
||||
*/
|
||||
function useBackButton(func) {
|
||||
const component = Component.current;
|
||||
|
||||
/**
|
||||
* Enables the func listener, overriding default back button behavior.
|
||||
*/
|
||||
function enable() {
|
||||
backButtonManager.addListener(component, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the func listener, restoring the default back button behavior if
|
||||
* no other listeners are present.
|
||||
*/
|
||||
function disable() {
|
||||
backButtonManager.removeListener(component, func);
|
||||
}
|
||||
|
||||
onWillUnmount(() => {
|
||||
disable();
|
||||
});
|
||||
|
||||
const superDestroy = component.destroy.bind(component);
|
||||
component.destroy = function () {
|
||||
disable();
|
||||
superDestroy();
|
||||
};
|
||||
|
||||
return {
|
||||
disable,
|
||||
enable,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
useBackButton,
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
odoo.define('odex25_web_mobile.Session', function (require) {
|
||||
"use strict";
|
||||
|
||||
const core = require('web.core');
|
||||
const Session = require('web.Session');
|
||||
const utils = require('web.utils');
|
||||
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
/*
|
||||
Android webview not supporting post download and odoo is using post method to download
|
||||
so here override get_file of session and passed all data to native mobile downloader
|
||||
ISSUE: https://code.google.com/p/android/issues/detail?id=1780
|
||||
*/
|
||||
|
||||
Session.include({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get_file: function (options) {
|
||||
if (mobile.methods.downloadFile) {
|
||||
if (core.csrf_token) {
|
||||
options.csrf_token = core.csrf_token;
|
||||
}
|
||||
mobile.methods.downloadFile(options);
|
||||
// There is no need to wait downloadFile because we delegate this to
|
||||
// Download Manager Service where error handling will be handled correclty.
|
||||
// On our side, we do not want to block the UI and consider the request
|
||||
// as success.
|
||||
if (options.success) { options.success(); }
|
||||
if (options.complete) { options.complete(); }
|
||||
return true;
|
||||
} else {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the user's account details on the mobile app
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async updateAccountOnMobileDevice() {
|
||||
if (!mobile.methods.updateAccount) {
|
||||
return;
|
||||
}
|
||||
const avatar = await this.fetchAvatar();
|
||||
const base64Avatar = await utils.getDataURLFromFile(avatar);
|
||||
return mobile.methods.updateAccount({
|
||||
avatar: base64Avatar.substring(base64Avatar.indexOf(',') + 1),
|
||||
name: this.name,
|
||||
username: this.username,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch current user's avatar
|
||||
*
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
async fetchAvatar() {
|
||||
const url = this.url('/web/image', {
|
||||
model: 'res.users',
|
||||
field: 'image_medium',
|
||||
id: this.uid,
|
||||
});
|
||||
const response = await fetch(url);
|
||||
return response.blob();
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
odoo.define('odex25_web_mobile.CrashManager', function (require) {
|
||||
"use strict";
|
||||
|
||||
var CrashManager = require('web.CrashManager').CrashManager;
|
||||
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
CrashManager.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
rpc_error: function (error) {
|
||||
if (mobile.methods.crashManager) {
|
||||
mobile.methods.crashManager(error);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
odoo.define("odex25_web_mobile.datepicker", function (require) {
|
||||
"use strict";
|
||||
|
||||
const mobile = require("odex25_web_mobile.core");
|
||||
const web_datepicker = require("web.datepicker");
|
||||
const Widget = require("web.Widget");
|
||||
|
||||
/**
|
||||
* Override odoo date-picker (bootstrap date-picker) to display mobile native
|
||||
* date picker. Because of it is better to show native mobile date-picker to
|
||||
* improve usability of Application (Due to Mobile users are used to native
|
||||
* date picker).
|
||||
*/
|
||||
web_datepicker.DateWidget.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start() {
|
||||
if (!mobile.methods.requestDateTimePicker) {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
this.$input = this.$("input.o_datepicker_input");
|
||||
// forcefully removes the library's classname to "disable" library's event listeners
|
||||
this.$input.removeClass('datetimepicker-input')
|
||||
this._setupMobilePicker();
|
||||
},
|
||||
|
||||
/**
|
||||
* Bootstrap date-picker already destroyed at initialization
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
if (!mobile.methods.requestDateTimePicker) {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
Widget.prototype.destroy.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
maxDate() {
|
||||
if (!mobile.methods.requestDateTimePicker) {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
console.warn("Unsupported in the mobile applications");
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
minDate() {
|
||||
if (!mobile.methods.requestDateTimePicker) {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
console.warn("Unsupported in the mobile applications");
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_setLibInputValue() {
|
||||
if (!mobile.methods.requestDateTimePicker) {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_setupMobilePicker() {
|
||||
this.$el.on("click", async () => {
|
||||
const { data } = await mobile.methods.requestDateTimePicker({
|
||||
value: this.getValue() ? this.getValue().format("YYYY-MM-DD HH:mm:ss") : false,
|
||||
type: this.type_of_date,
|
||||
ignore_timezone: true,
|
||||
});
|
||||
this.$input.val(data);
|
||||
this.changeDatetime();
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onInputClicked: function () {
|
||||
if (!mobile.methods.requestDateTimePicker) {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
odoo.define('odex25_web_mobile.FormRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormRenderer = require('web.FormRenderer');
|
||||
|
||||
var ContactSync = require('odex25_web_mobile.ContactSync');
|
||||
|
||||
/**
|
||||
* Include the FormRenderer to instanciate widget ContactSync.
|
||||
* The method will be automatically called to replace the tag <contactsync>.
|
||||
*/
|
||||
FormRenderer.include({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/** We always return a $el even if it's asynchronous (see _renderFieldWidget).
|
||||
*
|
||||
* @private
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_renderTagContactsync: function () {
|
||||
var $el = $('<div>');
|
||||
var widget = new ContactSync(this, {
|
||||
res_id: this.state.res_id,
|
||||
res_model: this.state.model,
|
||||
});
|
||||
// Prepare widget rendering and save the related promise
|
||||
var prom = widget._widgetRenderAndInsert(function () { });
|
||||
prom.then(function () {
|
||||
$el.replaceWith(widget.$el);
|
||||
});
|
||||
|
||||
this.widgets.push(widget);
|
||||
this.defs.push(prom);
|
||||
|
||||
return $el;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
odoo.define('odex25_web_mobile.FormView', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var FormView = require('web.FormView');
|
||||
var QuickCreateFormView = require('web.QuickCreateFormView');
|
||||
|
||||
/**
|
||||
* We don't want to see the keyboard after the opening of a form view.
|
||||
* The keyboard takes a lot of space and the user doesn't have a global view
|
||||
* on the form.
|
||||
* Plus, some relational fields are overrided (Many2One for example) and the
|
||||
* user have to click on it to set a value. On this kind of field, the autofocus
|
||||
* doesn't make sense because you can't directly type on it.
|
||||
* So, we have to disable the autofocus in mobile.
|
||||
*/
|
||||
FormView.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (config.device.isMobile) {
|
||||
this.controllerParams.disableAutofocus = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
QuickCreateFormView.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.controllerParams.disableAutofocus = false;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
odoo.define('odex25_web_mobile.PivotRenderer', async function (require) {
|
||||
'use strict';
|
||||
|
||||
const config = require('web.config');
|
||||
|
||||
if (!config.device.isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PivotRenderer = require('web.PivotRenderer');
|
||||
|
||||
|
||||
PivotRenderer.patch("pivot_mobile", T => class extends T {
|
||||
/**
|
||||
* Do not compute the tooltip on mobile
|
||||
* @override
|
||||
*/
|
||||
_updateTooltip() { }
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getPadding(cell) {
|
||||
return 5 + cell.indent * 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onClickMenuGroupBy(field, interval, ev) {
|
||||
if (!ev.currentTarget.classList.contains('o_pivot_field_selection')){
|
||||
super._onClickMenuGroupBy(...arguments);
|
||||
} else {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
odoo.define('odex25_web_mobile.core', function () {
|
||||
"use strict";
|
||||
|
||||
var available = typeof OdooDeviceUtility !== 'undefined';
|
||||
var DeviceUtility;
|
||||
var deferreds = {};
|
||||
var methods = {};
|
||||
|
||||
if (available){
|
||||
DeviceUtility = OdooDeviceUtility;
|
||||
delete window.OdooDeviceUtility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for invoking native methods which called from JavaScript
|
||||
*
|
||||
* @param {String} name name of action want to perform in mobile
|
||||
* @param {Object} args extra arguments for mobile
|
||||
*
|
||||
* @returns Promise Object
|
||||
*/
|
||||
function native_invoke(name, args) {
|
||||
if(_.isUndefined(args)){
|
||||
args = {};
|
||||
}
|
||||
var id = _.uniqueId();
|
||||
args = JSON.stringify(args);
|
||||
DeviceUtility.execute(name, args, id);
|
||||
return new Promise(function (resolve, reject) {
|
||||
deferreds[id] = {
|
||||
successCallback: resolve,
|
||||
errorCallback: reject
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages deferred callback from initiate from native mobile
|
||||
*
|
||||
* @param {String} id callback id
|
||||
* @param {Object} result
|
||||
*/
|
||||
window.odoo.native_notify = function (id, result) {
|
||||
if (deferreds.hasOwnProperty(id)) {
|
||||
if (result.success) {
|
||||
deferreds[id].successCallback(result);
|
||||
} else {
|
||||
deferreds[id].errorCallback(result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var plugins = available ? JSON.parse(DeviceUtility.list_plugins()) : [];
|
||||
_.each(plugins, function (plugin) {
|
||||
methods[plugin.name] = function (args) {
|
||||
return native_invoke(plugin.action, args);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Use to notify an uri hash change on native devices (ios / android)
|
||||
*/
|
||||
if (methods.hashChange) {
|
||||
var currentHash;
|
||||
$(window).bind('hashchange', function (event) {
|
||||
var hash = event.getState();
|
||||
if (!_.isEqual(currentHash, hash)) {
|
||||
methods.hashChange(hash);
|
||||
}
|
||||
currentHash = hash;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* By using the back button feature the default back button behavior from the
|
||||
* app is actually overridden so it is important to keep count to restore the
|
||||
* default when no custom listener are remaining.
|
||||
*/
|
||||
class BackButtonManager {
|
||||
|
||||
constructor() {
|
||||
this._listeners = new Map();
|
||||
this._onGlobalBackButton = this._onGlobalBackButton.bind(this);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Enables the func listener, overriding default back button behavior.
|
||||
*
|
||||
* @param {Widget|Component} listener
|
||||
* @param {function} func
|
||||
*/
|
||||
addListener(listener, func) {
|
||||
if (!methods.overrideBackButton) {
|
||||
return;
|
||||
}
|
||||
if (this._listeners.has(listener)) {
|
||||
throw new Error("This listener was already registered.");
|
||||
}
|
||||
this._listeners.set(listener, func);
|
||||
if (this._listeners.size === 1) {
|
||||
document.addEventListener('backbutton', this._onGlobalBackButton);
|
||||
methods.overrideBackButton({ enabled: true });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Disables the func listener, restoring the default back button behavior if
|
||||
* no other listeners are present.
|
||||
*
|
||||
* @param {Widget|Component} listener
|
||||
* @param {function} func
|
||||
*/
|
||||
removeListener(listener, func) {
|
||||
if (!methods.overrideBackButton) {
|
||||
return;
|
||||
}
|
||||
if (!this._listeners.has(listener)) {
|
||||
return;
|
||||
}
|
||||
this._listeners.delete(listener);
|
||||
if (this._listeners.size === 0) {
|
||||
document.removeEventListener('backbutton', this._onGlobalBackButton);
|
||||
methods.overrideBackButton({ enabled: false });
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_onGlobalBackButton() {
|
||||
const [listener, func] = [...this._listeners].pop();
|
||||
if (listener) {
|
||||
func.apply(listener, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const backButtonManager = new BackButtonManager();
|
||||
|
||||
return {
|
||||
backButtonManager,
|
||||
methods,
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
odoo.define('odex25_web_mobile.user_menu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var UserMenu = require('web.UserMenu');
|
||||
var web_client = require('web.web_client');
|
||||
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
// Hide the logout link in mobile
|
||||
UserMenu.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
if (mobile.methods.switchAccount) {
|
||||
self.$('a[data-menu="logout"]').addClass('d-none');
|
||||
self.$('a[data-menu="account"]').addClass('d-none');
|
||||
self.$('a[data-menu="switch"]').removeClass('d-none');
|
||||
}
|
||||
if (mobile.methods.addHomeShortcut) {
|
||||
self.$('a[data-menu="shortcut"]').removeClass('d-none');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onMenuSwitch: function () {
|
||||
mobile.methods.switchAccount();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onMenuShortcut: function () {
|
||||
var urlData = $.bbq.getState();
|
||||
if (urlData.menu_id) {
|
||||
var menus = web_client.menu.menu_data;
|
||||
var menu = _.filter(menus.children, function (child) {
|
||||
return child.id === parseInt(urlData.menu_id);
|
||||
});
|
||||
mobile.methods.addHomeShortcut({
|
||||
'title': document.title,
|
||||
'shortcut_url': document.URL,
|
||||
'web_icon': menu && menu[0].web_icon_data
|
||||
});
|
||||
} else {
|
||||
mobile.methods.showToast({
|
||||
"message": _t("No shortcut for Home Menu")
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
odoo.define('odex25_web_mobile.UserPreferencesFormView', function (require) {
|
||||
'use strict';
|
||||
|
||||
const FormView = require('web.FormView');
|
||||
const viewRegistry = require('web.view_registry');
|
||||
const { UpdateDeviceAccountControllerMixin } = require('odex25_web_mobile.mixins');
|
||||
|
||||
const UserPreferencesFormView = FormView.extend({
|
||||
config: Object.assign({}, FormView.prototype.config, {
|
||||
Controller: FormView.prototype.config.Controller.extend(
|
||||
UpdateDeviceAccountControllerMixin
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('res_users_preferences_form', UserPreferencesFormView);
|
||||
|
||||
return UserPreferencesFormView;
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.o_barcode_mobile {
|
||||
-webkit-mask: url('/odex25_web_mobile/static/src/img/barcode.svg') center/contain no-repeat;
|
||||
mask: url('/odex25_web_mobile/static/src/img/barcode.svg') center/contain no-repeat;
|
||||
background-color: $o-enterprise-primary-color;
|
||||
width: 26px;
|
||||
margin-left: 10px;
|
||||
border: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
@include media-breakpoint-down(sm) {
|
||||
.o_pivot {
|
||||
.o_pivot_field_menu {
|
||||
left: 0 !important;
|
||||
|
||||
.dropdown-item {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
&.o_pivot_field_selection::after{
|
||||
top:21px;
|
||||
}
|
||||
}
|
||||
|
||||
// css for open child sub menu
|
||||
&.show > .o_inline_dropdown > .dropdown-menu {
|
||||
top: initial !important;
|
||||
left: 5% !important;
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
.o_pivot_field_selection::after {
|
||||
@include o-caret-down;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="FieldMany2OneBarcode" t-extend="FieldMany2One">
|
||||
<t t-jquery="button.o_external_button " t-operation="after">
|
||||
<button
|
||||
type="button"
|
||||
class="btn o_barcode_mobile"
|
||||
tabindex="-1"
|
||||
draggable="false"
|
||||
aria-label="Scan barcode"
|
||||
title="Scan barcode"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="ContactSync">
|
||||
<button t-attf-class="btn oe_stat_button"
|
||||
widget="contact_sync" t-if="widget.is_mobile">
|
||||
<i class="fa fa-fw fa-user-plus o_button_icon" role="img" aria-label="Add" title="Add"></i>
|
||||
<span class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text text-primary">Add to<br/>Mobile</span>
|
||||
</span>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="DashboardMain">
|
||||
<t t-jquery=".o_web_settings_dashboard_share" t-operation="after">
|
||||
<div class="mt8 row mr0 ml0">
|
||||
<div class="col-lg-6 col-6 col-md-6">
|
||||
<a class="d-block mx-auto" href="https://play.google.com/store/apps/details?id=com.odoo.mobile" target="_blank">
|
||||
<img class="d-block mx-auto img img-fluid" src="/odex25_web_mobile/static/src/img/google_play.png" alt="On Google Play"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-6 col-6 col-md-6">
|
||||
<a class="d-block mx-auto" href="https://itunes.apple.com/us/app/odoo/id1272543640" target="_blank">
|
||||
<img class="d-block mx-auto img img-fluid" src="/odex25_web_mobile/static/src/img/app_store.png" alt="On Apple Store"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-extend="UserMenu.Actions">
|
||||
<t t-jquery=".dropdown-item:last" t-operation="after">
|
||||
<a href="#" class="dropdown-item d-none" data-menu="shortcut">Add to Home Screen</a>
|
||||
<a href="#" class="dropdown-item d-none" data-menu="switch"> Switch/Add Account</a>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
odoo.define('odex25_web_mobile.testUtils', function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Transforms base64 encoded data to a Blob object
|
||||
*
|
||||
* @param {string} b64Data
|
||||
* @param {string} contentType
|
||||
* @param {int} sliceSize
|
||||
* @returns {Blob}
|
||||
*/
|
||||
function base64ToBlob(b64Data, contentType, sliceSize) {
|
||||
contentType = contentType || '';
|
||||
sliceSize = sliceSize || 512;
|
||||
|
||||
const byteCharacters = atob(b64Data);
|
||||
let byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
const byteNumbers = Array.from(slice).map((char) => char.charCodeAt(0));
|
||||
byteArrays.push(new Uint8Array(byteNumbers));
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, { type: contentType });
|
||||
}
|
||||
|
||||
return {
|
||||
base64ToBlob,
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
odoo.define('odex25_web_mobile.barcode.tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var field_registry = require('web.field_registry');
|
||||
var FormView = require('web.FormView');
|
||||
var relational_fields = require('web.relational_fields');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var barcode_fields = require('odex25_web_mobile.barcode_fields');
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
var NAME_SEARCH = "name_search";
|
||||
var PRODUCT_PRODUCT = 'product.product';
|
||||
var SALE_ORDER_LINE = 'sale_order_line';
|
||||
var PRODUCT_FIELD_NAME = 'product_id';
|
||||
var ARCHS = {
|
||||
'product.product,false,kanban': '<kanban>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div class="oe_kanban_global_click"><field name="display_name"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
'product.product,false,search': '<search></search>',
|
||||
};
|
||||
|
||||
QUnit.module('odex25_web_mobile', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
[PRODUCT_PRODUCT]: {
|
||||
fields: {
|
||||
id: {type: 'integer'},
|
||||
name: {},
|
||||
barcode: {},
|
||||
},
|
||||
records: [{
|
||||
id: 111,
|
||||
name: 'product_cable_management_box',
|
||||
barcode: '601647855631',
|
||||
}]
|
||||
},
|
||||
[SALE_ORDER_LINE]: {
|
||||
fields: {
|
||||
id: {type: 'integer'},
|
||||
[PRODUCT_FIELD_NAME]: {
|
||||
string: PRODUCT_FIELD_NAME,
|
||||
type: 'many2one',
|
||||
relation: PRODUCT_PRODUCT
|
||||
},
|
||||
product_uom_qty: {type: 'integer'}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test("odex25_web_mobile: barcode button in a mobile environment", async function (assert) {
|
||||
var self = this;
|
||||
|
||||
assert.expect(3);
|
||||
|
||||
// simulate a mobile environment
|
||||
field_registry.add('many2one_barcode', barcode_fields);
|
||||
var __scanBarcode = mobile.methods.scanBarcode;
|
||||
var __showToast = mobile.methods.showToast;
|
||||
var __vibrate = mobile.methods.vibrate;
|
||||
|
||||
mobile.methods.scanBarcode = function () {
|
||||
return Promise.resolve({
|
||||
'data': self
|
||||
.data[PRODUCT_PRODUCT]
|
||||
.records[0]
|
||||
.barcode
|
||||
});
|
||||
};
|
||||
|
||||
mobile.methods.showToast = function (data) {};
|
||||
|
||||
mobile.methods.vibrate = function () {};
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="' + PRODUCT_FIELD_NAME + '" widget="many2one_barcode"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
data: this.data,
|
||||
model: SALE_ORDER_LINE,
|
||||
archs: ARCHS,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === NAME_SEARCH && args.model === PRODUCT_PRODUCT) {
|
||||
return this._super.apply(this, arguments).then(function (result) {
|
||||
var records = self
|
||||
.data[PRODUCT_PRODUCT]
|
||||
.records
|
||||
.filter(function (record) {
|
||||
return record.barcode === args.kwargs.name;
|
||||
})
|
||||
.map(function (record) {
|
||||
return [record.id, record.name];
|
||||
})
|
||||
;
|
||||
return records.concat(result);
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var $scanButton = form.$('.o_barcode_mobile');
|
||||
|
||||
assert.equal($scanButton.length, 1, "has scanner button");
|
||||
|
||||
await testUtils.dom.click($scanButton);
|
||||
|
||||
var $modal = $('.o_modal_full .modal-lg');
|
||||
assert.equal($modal.length, 1, 'there should be one modal opened in full screen');
|
||||
|
||||
await testUtils.dom.click($modal.find('.o_kanban_view .o_kanban_record:first'));
|
||||
|
||||
var selectedId = form.renderer.state.data[PRODUCT_FIELD_NAME].res_id;
|
||||
assert.equal(selectedId, self.data[PRODUCT_PRODUCT].records[0].id,
|
||||
"product found and selected (" +
|
||||
self.data[PRODUCT_PRODUCT].records[0].barcode + ")");
|
||||
|
||||
mobile.methods.vibrate = __vibrate;
|
||||
mobile.methods.showToast = __showToast;
|
||||
mobile.methods.scanBarcode = __scanBarcode;
|
||||
field_registry.add('many2one_barcode', relational_fields.FieldMany2One);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,662 @@
|
|||
odoo.define('odex25_web_mobile.tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const Dialog = require('web.Dialog');
|
||||
const dom = require('web.dom');
|
||||
const FormView = require('web.FormView');
|
||||
const KanbanView = require('web.KanbanView');
|
||||
const session = require('web.session');
|
||||
const testUtils = require('web.test_utils');
|
||||
const Widget = require('web.Widget');
|
||||
|
||||
const { useBackButton } = require('odex25_web_mobile.hooks');
|
||||
const { BackButtonEventMixin, UpdateDeviceAccountControllerMixin } = require('odex25_web_mobile.mixins');
|
||||
const mobile = require('odex25_web_mobile.core');
|
||||
const UserPreferencesFormView = require('odex25_web_mobile.UserPreferencesFormView');
|
||||
const { base64ToBlob } = require('odex25_web_mobile.testUtils');
|
||||
|
||||
const { Component, tags } = owl;
|
||||
const { xml } = tags;
|
||||
|
||||
const {createParent, createView, mock} = testUtils;
|
||||
|
||||
const MY_IMAGE = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
||||
|
||||
QUnit.module('odex25_web_mobile', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
name: {string: "name", type: "char"},
|
||||
image_1920: {},
|
||||
parent_id: {string: "Parent", type: "many2one", relation: 'partner'},
|
||||
sibling_ids: {string: "Sibling", type: "many2many", relation: 'partner'},
|
||||
phone: {},
|
||||
mobile: {},
|
||||
email: {},
|
||||
street: {},
|
||||
street2: {},
|
||||
city: {},
|
||||
state_id: {},
|
||||
zip: {},
|
||||
country_id: {},
|
||||
website: {},
|
||||
function: {},
|
||||
title: {},
|
||||
date: {string: "A date", type: "date"},
|
||||
datetime: {string: "A datetime", type: "datetime"},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
name: 'coucou1',
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'coucou2',
|
||||
}, {
|
||||
id: 11,
|
||||
name: 'coucou3',
|
||||
image_1920: 'image',
|
||||
parent_id: 1,
|
||||
phone: 'phone',
|
||||
mobile: 'mobile',
|
||||
email: 'email',
|
||||
street: 'street',
|
||||
street2: 'street2',
|
||||
city: 'city',
|
||||
state_id: 'state_id',
|
||||
zip: 'zip',
|
||||
country_id: 'country_id',
|
||||
website: 'website',
|
||||
function: 'function',
|
||||
title: 'title',
|
||||
}],
|
||||
},
|
||||
users: {
|
||||
fields: {
|
||||
name: { string: "name", type: "char" },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test("contact sync in a non-mobile environment", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
let rpcCount = 0;
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<div name="button_box">' +
|
||||
'<contactsync> </contactsync>' +
|
||||
'</div>' +
|
||||
'<field name="name"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
data: this.data,
|
||||
model: 'partner',
|
||||
mockRPC: function () {
|
||||
rpcCount++;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
res_id: 11,
|
||||
});
|
||||
|
||||
const $button = form.$('button.oe_stat_button[widget="contact_sync"]');
|
||||
|
||||
assert.strictEqual($button.length, 0, "the tag should not be visible in a non-mobile environment");
|
||||
assert.strictEqual(rpcCount, 1, "no extra rpc should be done by the widget (only the one from the view)");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("contact sync in a mobile environment", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
|
||||
const __addContact = mobile.methods.addContact;
|
||||
let addContactRecord;
|
||||
// override addContact to simulate a mobile environment
|
||||
mobile.methods.addContact = function (r) {
|
||||
addContactRecord = r;
|
||||
};
|
||||
|
||||
let rpcDone;
|
||||
let rpcCount = 0;
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<sheet>' +
|
||||
'<div name="button_box">' +
|
||||
'<contactsync> </contactsync>' +
|
||||
'</div>' +
|
||||
'<field name="name"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
data: this.data,
|
||||
model: 'partner',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "read" && args.args[0] === 11 && _.contains(args.args[1], 'phone')) {
|
||||
rpcDone = true;
|
||||
}
|
||||
rpcCount++;
|
||||
return this._super(route, args);
|
||||
},
|
||||
res_id: 11,
|
||||
});
|
||||
|
||||
const $button = form.$('button.oe_stat_button[widget="contact_sync"]');
|
||||
|
||||
assert.strictEqual($button.length, 1, "the tag should be visible in a mobile environment");
|
||||
assert.strictEqual(rpcCount, 1, "no extra rpc should be done by the widget (only the one from the view)");
|
||||
|
||||
await testUtils.dom.click($button);
|
||||
|
||||
assert.strictEqual(rpcCount, 2, "an extra rpc should be done on click");
|
||||
assert.ok(rpcDone, "a read rpc should have been done");
|
||||
assert.deepEqual(addContactRecord, {
|
||||
city: "city",
|
||||
country_id: "country_id",
|
||||
email: "email",
|
||||
function: "function",
|
||||
id: 11,
|
||||
image: "image",
|
||||
mobile: "mobile",
|
||||
name: "coucou3",
|
||||
parent_id: [
|
||||
1,
|
||||
"coucou1",
|
||||
],
|
||||
phone: "phone",
|
||||
state_id: "state_id",
|
||||
street: "street",
|
||||
street2: "street2",
|
||||
website: "website",
|
||||
zip: "zip"
|
||||
}, "all data should be correctly passed");
|
||||
|
||||
mobile.methods.addContact = __addContact;
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('autofocus quick create form', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const kanban = await createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban on_create="quick_create">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="name"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['parent_id'],
|
||||
});
|
||||
|
||||
// quick create in first column
|
||||
await testUtils.dom.click(kanban.$buttons.find('.o-kanban-button-new'));
|
||||
assert.ok(kanban.$('.o_kanban_group:nth(0) > div:nth(1)').hasClass('o_kanban_quick_create'),
|
||||
"clicking on create should open the quick_create in the first column");
|
||||
assert.strictEqual(document.activeElement, kanban.$('.o_kanban_quick_create .o_input:first')[0],
|
||||
"the first input field should get the focus when the quick_create is opened");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('BackButtonEventMixin');
|
||||
|
||||
QUnit.test('widget should receive a backbutton event', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const __overrideBackButton = mobile.methods.overrideBackButton;
|
||||
mobile.methods.overrideBackButton = function ({enabled}) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
};
|
||||
|
||||
const DummyWidget = Widget.extend(BackButtonEventMixin, {
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${ev.type} event`);
|
||||
},
|
||||
});
|
||||
const backButtonEvent = new Event('backbutton');
|
||||
const dummy = new DummyWidget();
|
||||
dummy.appendTo($('<div>'));
|
||||
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
// waiting nextTick to match testUtils.dom.triggerEvents() behavior
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([], "shouldn't have register handle before attached to the DOM");
|
||||
|
||||
dom.append($('qunit-fixture'), dummy.$el, {in_DOM: true, callbacks: [{widget: dummy}]});
|
||||
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
await testUtils.nextTick();
|
||||
|
||||
dom.detach([{widget: dummy}]);
|
||||
|
||||
assert.verifySteps([
|
||||
'overrideBackButton: true',
|
||||
'backbutton event',
|
||||
'overrideBackButton: false',
|
||||
], "should have enabled/disabled the back-button override");
|
||||
|
||||
dummy.destroy();
|
||||
mobile.methods.overrideBackButton = __overrideBackButton;
|
||||
});
|
||||
|
||||
QUnit.test('multiple widgets should receive backbutton events in the right order', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const __overrideBackButton = mobile.methods.overrideBackButton;
|
||||
mobile.methods.overrideBackButton = function ({enabled}) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
};
|
||||
|
||||
const DummyWidget = Widget.extend(BackButtonEventMixin, {
|
||||
init(parent, {name}) {
|
||||
this._super.apply(this, arguments);
|
||||
this.name = name;
|
||||
},
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${this.name}: ${ev.type} event`);
|
||||
dom.detach([{widget: this}]);
|
||||
},
|
||||
});
|
||||
const backButtonEvent = new Event('backbutton');
|
||||
const dummy1 = new DummyWidget(null, {name: 'dummy1'});
|
||||
dom.append($('qunit-fixture'), dummy1.$el, {in_DOM: true, callbacks: [{widget: dummy1}]});
|
||||
|
||||
const dummy2 = new DummyWidget(null, {name: 'dummy2'});
|
||||
dom.append($('qunit-fixture'), dummy2.$el, {in_DOM: true, callbacks: [{widget: dummy2}]});
|
||||
|
||||
const dummy3 = new DummyWidget(null, {name: 'dummy3'});
|
||||
dom.append($('qunit-fixture'), dummy3.$el, {in_DOM: true, callbacks: [{widget: dummy3}]});
|
||||
|
||||
// simulate 'backbutton' events triggered by the app
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
// waiting nextTick to match testUtils.dom.triggerEvents() behavior
|
||||
await testUtils.nextTick();
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
await testUtils.nextTick();
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([
|
||||
'overrideBackButton: true',
|
||||
'dummy3: backbutton event',
|
||||
'dummy2: backbutton event',
|
||||
'dummy1: backbutton event',
|
||||
'overrideBackButton: false',
|
||||
]);
|
||||
|
||||
dummy1.destroy();
|
||||
dummy2.destroy();
|
||||
dummy3.destroy();
|
||||
mobile.methods.overrideBackButton = __overrideBackButton;
|
||||
});
|
||||
|
||||
QUnit.module('useBackButton');
|
||||
|
||||
QUnit.test('component should receive a backbutton event', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
mock.patch(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
class DummyComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this._backButtonHandler = useBackButton(this._onBackButton);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this._backButtonHandler.enable();
|
||||
}
|
||||
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${ev.type} event`);
|
||||
}
|
||||
}
|
||||
DummyComponent.template = xml`<div/>`;
|
||||
|
||||
const dummy = new DummyComponent();
|
||||
|
||||
await dummy.mount(document.createDocumentFragment());
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, 'backbutton');
|
||||
assert.verifySteps([], "shouldn't have register handle before attached to the DOM");
|
||||
dummy.unmount();
|
||||
|
||||
await dummy.mount(document.getElementById('qunit-fixture'));
|
||||
// simulate 'backbutton' event triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, 'backbutton');
|
||||
dummy.unmount();
|
||||
assert.verifySteps([
|
||||
'overrideBackButton: true',
|
||||
'backbutton event',
|
||||
'overrideBackButton: false',
|
||||
], "should have enabled/disabled the back-button override");
|
||||
|
||||
dummy.destroy();
|
||||
mock.unpatch(mobile.methods);
|
||||
});
|
||||
|
||||
QUnit.test('multiple components should receive backbutton events in the right order', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
mock.patch(mobile.methods, {
|
||||
overrideBackButton({ enabled }) {
|
||||
assert.step(`overrideBackButton: ${enabled}`);
|
||||
},
|
||||
});
|
||||
|
||||
class DummyComponent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this._backButtonHandler = useBackButton(this._onBackButton);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this._backButtonHandler.enable();
|
||||
}
|
||||
|
||||
_onBackButton(ev) {
|
||||
assert.step(`${this.props.name}: ${ev.type} event`);
|
||||
this.unmount();
|
||||
}
|
||||
}
|
||||
DummyComponent.template = xml`<div/>`;
|
||||
|
||||
const fixture = document.getElementById('qunit-fixture');
|
||||
|
||||
const dummy1 = new DummyComponent(null, { name: 'dummy1'});
|
||||
await dummy1.mount(fixture);
|
||||
|
||||
const dummy2 = new DummyComponent(null, { name: 'dummy2'});
|
||||
await dummy2.mount(fixture);
|
||||
|
||||
const dummy3 = new DummyComponent(null, { name: 'dummy3'});
|
||||
await dummy3.mount(fixture);
|
||||
|
||||
// simulate 'backbutton' events triggered by the app
|
||||
await testUtils.dom.triggerEvent(document, 'backbutton');
|
||||
await testUtils.dom.triggerEvent(document, 'backbutton');
|
||||
await testUtils.dom.triggerEvent(document, 'backbutton');
|
||||
|
||||
assert.verifySteps([
|
||||
'overrideBackButton: true',
|
||||
'dummy3: backbutton event',
|
||||
'dummy2: backbutton event',
|
||||
'dummy1: backbutton event',
|
||||
'overrideBackButton: false',
|
||||
]);
|
||||
|
||||
dummy1.destroy();
|
||||
dummy2.destroy();
|
||||
dummy3.destroy();
|
||||
mock.unpatch(mobile.methods);
|
||||
});
|
||||
|
||||
QUnit.module('Dialog');
|
||||
|
||||
QUnit.test('dialog is closable with backbutton event', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const __overrideBackButton = mobile.methods.overrideBackButton;
|
||||
mobile.methods.overrideBackButton = function () {};
|
||||
|
||||
testUtils.mock.patch(Dialog, {
|
||||
close: function () {
|
||||
assert.step("close");
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
const parent = await createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,form': `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const backButtonEvent = new Event('backbutton');
|
||||
const dialog = new Dialog(parent, {
|
||||
res_model: 'partner',
|
||||
res_id: 1,
|
||||
}).open();
|
||||
await dialog.opened().then(() => {
|
||||
assert.step('opened');
|
||||
});
|
||||
assert.containsOnce(document.body, '.modal', "should have a modal");
|
||||
|
||||
// simulate 'backbutton' event triggered by the app waiting
|
||||
document.dispatchEvent(backButtonEvent);
|
||||
// nextTick to match testUtils.dom.triggerEvents() behavior
|
||||
await testUtils.nextTick();
|
||||
|
||||
// The goal of this assert is to check that our event called the
|
||||
// opened/close methods on Dialog.
|
||||
assert.verifySteps([
|
||||
'opened',
|
||||
'close',
|
||||
], "should have open/close dialog");
|
||||
assert.containsNone(document.body, '.modal', "modal should be closed");
|
||||
|
||||
parent.destroy();
|
||||
testUtils.mock.unpatch(Dialog);
|
||||
mobile.methods.overrideBackButton = __overrideBackButton;
|
||||
});
|
||||
|
||||
QUnit.module('UpdateDeviceAccountControllerMixin');
|
||||
|
||||
QUnit.test('controller should call native updateAccount method when saving record', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const __updateAccount = mobile.methods.updateAccount;
|
||||
mobile.methods.updateAccount = function (options) {
|
||||
const { avatar, name, username } = options;
|
||||
assert.ok("should call updateAccount");
|
||||
assert.strictEqual(avatar, MY_IMAGE, "should have a base64 encoded avatar");
|
||||
assert.strictEqual(name, "Marc Demo");
|
||||
assert.strictEqual(username, "demo");
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
testUtils.mock.patch(session, {
|
||||
fetchAvatar() {
|
||||
return Promise.resolve(base64ToBlob(MY_IMAGE, 'image/png'));
|
||||
},
|
||||
});
|
||||
|
||||
const DummyView = FormView.extend({
|
||||
config: Object.assign({}, FormView.prototype.config, {
|
||||
Controller: FormView.prototype.config.Controller.extend(UpdateDeviceAccountControllerMixin),
|
||||
}),
|
||||
});
|
||||
|
||||
const dummy = await createView({
|
||||
View: DummyView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
session: {
|
||||
username: "demo",
|
||||
name: "Marc Demo",
|
||||
}
|
||||
});
|
||||
|
||||
await testUtils.form.clickSave(dummy);
|
||||
await dummy.savingDef;
|
||||
|
||||
dummy.destroy();
|
||||
testUtils.mock.unpatch(session);
|
||||
mobile.methods.updateAccount = __updateAccount;
|
||||
});
|
||||
|
||||
QUnit.test('UserPreferencesFormView should call native updateAccount method when saving record', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const __updateAccount = mobile.methods.updateAccount;
|
||||
mobile.methods.updateAccount = function (options) {
|
||||
const { avatar, name, username } = options;
|
||||
assert.ok("should call updateAccount");
|
||||
assert.strictEqual(avatar, MY_IMAGE, "should have a base64 encoded avatar");
|
||||
assert.strictEqual(name, "Marc Demo");
|
||||
assert.strictEqual(username, "demo");
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
testUtils.mock.patch(session, {
|
||||
fetchAvatar() {
|
||||
return Promise.resolve(base64ToBlob(MY_IMAGE, 'image/png'));
|
||||
},
|
||||
});
|
||||
|
||||
const view = await createView({
|
||||
View: UserPreferencesFormView,
|
||||
model: 'users',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
session: {
|
||||
username: "demo",
|
||||
name: "Marc Demo",
|
||||
}
|
||||
});
|
||||
|
||||
await testUtils.form.clickSave(view);
|
||||
await view.savingDef;
|
||||
|
||||
view.destroy();
|
||||
testUtils.mock.unpatch(session);
|
||||
mobile.methods.updateAccount = __updateAccount;
|
||||
});
|
||||
|
||||
QUnit.module('FieldDate');
|
||||
|
||||
QUnit.test('date field: toggle datepicker', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
mock.patch(mobile.methods, {
|
||||
requestDateTimePicker({ value, type }) {
|
||||
assert.step("requestDateTimePicker");
|
||||
assert.strictEqual(false, value, "field shouldn't have an initial value");
|
||||
assert.strictEqual("date", type, "datepicker's mode should be 'date'");
|
||||
return Promise.resolve({ data: "2020-01-12", });
|
||||
},
|
||||
});
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form><field name="date"/><field name="name"/></form>',
|
||||
translateParameters: { // Avoid issues due to localization formats
|
||||
date_format: '%m/%d/%Y',
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsNone(document.body, '.bootstrap-datetimepicker-widget',
|
||||
"datepicker shouldn't be present initially");
|
||||
|
||||
await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
|
||||
|
||||
assert.containsNone(document.body, '.bootstrap-datetimepicker-widget',
|
||||
"datepicker shouldn't be opened");
|
||||
assert.verifySteps(["requestDateTimePicker"], "native datepicker should have been called");
|
||||
// ensure focus has been restored to the date field
|
||||
form.$('.o_datepicker_input').focus();
|
||||
assert.strictEqual(form.$('.o_datepicker_input').val(), "01/12/2020",
|
||||
"should be properly formatted");
|
||||
|
||||
// focus another field
|
||||
await testUtils.dom.click(form.$('.o_field_widget[name=name]').focus());
|
||||
assert.strictEqual(form.$('.o_datepicker_input').val(), "01/12/2020",
|
||||
"shouldn't have changed after loosing focus");
|
||||
|
||||
form.destroy();
|
||||
mock.unpatch(mobile.methods);
|
||||
});
|
||||
|
||||
QUnit.module('FieldDateTime');
|
||||
|
||||
QUnit.test('datetime field: toggle datepicker', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
mock.patch(mobile.methods, {
|
||||
requestDateTimePicker({ value, type }) {
|
||||
assert.step("requestDateTimePicker");
|
||||
assert.strictEqual(false, value, "field shouldn't have an initial value");
|
||||
assert.strictEqual("datetime", type, "datepicker's mode should be 'datetime'");
|
||||
return Promise.resolve({ data: "2020-01-12 12:00:00" });
|
||||
},
|
||||
});
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form><field name="datetime"/><field name="name"/></form>',
|
||||
translateParameters: { // Avoid issues due to localization formats
|
||||
date_format: '%m/%d/%Y',
|
||||
time_format: '%H:%M:%S',
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsNone(document.body, '.bootstrap-datetimepicker-widget',
|
||||
"datepicker shouldn't be present initially");
|
||||
|
||||
await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
|
||||
|
||||
assert.containsNone(document.body, '.bootstrap-datetimepicker-widget',
|
||||
"datepicker shouldn't be opened");
|
||||
assert.verifySteps(["requestDateTimePicker"], "native datepicker should have been called");
|
||||
// ensure focus has been restored to the datetime field
|
||||
form.$('.o_datepicker_input').focus();
|
||||
assert.strictEqual(form.$('.o_datepicker_input').val(), "01/12/2020 12:00:00",
|
||||
"should be properly formatted");
|
||||
|
||||
// focus another field
|
||||
await testUtils.dom.click(form.$('.o_field_widget[name=name]').focus());
|
||||
assert.strictEqual(form.$('.o_datepicker_input').val(), "01/12/2020 12:00:00",
|
||||
"shouldn't have changed after loosing focus");
|
||||
|
||||
form.destroy();
|
||||
mock.unpatch(mobile.methods);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import test_mobile_routes
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
from odoo.tools import config, mute_logger
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class MobileRoutesTest(HttpCase):
|
||||
"""
|
||||
This test suite is used to request the routes used by the mobile applications (Android & iOS)
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(MobileRoutesTest, self).setUp()
|
||||
self.headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def test_version_info(self):
|
||||
"""
|
||||
This request is used to check for a compatible Odoo server
|
||||
"""
|
||||
payload = self._build_payload()
|
||||
response = self.url_open(
|
||||
"/web/webclient/version_info",
|
||||
data=json.dumps(payload),
|
||||
headers=self.headers,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self._is_success_json_response(data)
|
||||
result = data["result"]
|
||||
self.assertIn("server_version_info", result)
|
||||
self.assertIsInstance(result["server_version_info"], list)
|
||||
self.assertGreater(len(result["server_version_info"]), 0)
|
||||
self.assertEqual(result["server_version_info"][-1], "e")
|
||||
|
||||
@mute_logger("odoo.http")
|
||||
def test_database_list(self):
|
||||
"""
|
||||
This request is used to retrieve the databases' list
|
||||
NB: this route has a different behavior depending on the ability to list databases or not.
|
||||
"""
|
||||
payload = self._build_payload()
|
||||
response = self.url_open("/web/database/list", data=json.dumps(payload), headers=self.headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
if config['list_db']:
|
||||
self._is_success_json_response(data)
|
||||
result = data["result"]
|
||||
self.assertIsInstance(result, list)
|
||||
self.assertGreater(len(result), 0)
|
||||
self.assertIn(self.env.cr.dbname, result)
|
||||
else:
|
||||
self._is_error_json_response(data)
|
||||
error = data["error"]
|
||||
self.assertEqual(error["code"], 200)
|
||||
self.assertEqual(error["message"], "Odoo Server Error")
|
||||
self.assertEqual(error["data"]["name"], "odoo.exceptions.AccessDenied")
|
||||
|
||||
def test_authenticate(self):
|
||||
"""
|
||||
This request is used to authenticate a user using its username/password
|
||||
and retrieve its details & session's id
|
||||
"""
|
||||
payload = self._build_payload({
|
||||
"db": self.env.cr.dbname,
|
||||
"login": "demo",
|
||||
"password": "demo",
|
||||
"context": {},
|
||||
})
|
||||
response = self.url_open("/web/session/authenticate", data=json.dumps(payload), headers=self.headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self._is_success_json_response(data)
|
||||
result = data["result"]
|
||||
self.assertIsInstance(response.cookies.get("session_id"), str, "should have a session cookie")
|
||||
self.assertEqual(result["username"], "demo")
|
||||
self.assertEqual(result["db"], self.env.cr.dbname)
|
||||
user = self.env["res.users"].search_read([("login", "=", "demo")], limit=1)[0]
|
||||
self.assertEqual(result["uid"], user["id"])
|
||||
self.assertEqual(result["name"], user["name"])
|
||||
|
||||
@mute_logger("odoo.http")
|
||||
def test_authenticate_wrong_credentials(self):
|
||||
"""
|
||||
This request is used to attempt to authenticate a user using the wrong credentials
|
||||
(username/password) and check the returned error
|
||||
"""
|
||||
payload = self._build_payload({
|
||||
"db": self.env.cr.dbname,
|
||||
"login": "demo",
|
||||
"password": "admin",
|
||||
"context": {},
|
||||
})
|
||||
response = self.url_open("/web/session/authenticate", data=json.dumps(payload), headers=self.headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self._is_error_json_response(data)
|
||||
error = data["error"]
|
||||
self.assertEqual(error["code"], 200)
|
||||
self.assertEqual(error["message"], "Odoo Server Error")
|
||||
self.assertEqual(error["data"]["name"], "odoo.exceptions.AccessDenied")
|
||||
|
||||
@mute_logger("odoo.http")
|
||||
def test_authenticate_wrong_database(self):
|
||||
"""
|
||||
This request is used to authenticate a user against a non existing database and
|
||||
check the returned error
|
||||
"""
|
||||
db_name = "dummydb-%s" % str(uuid4())
|
||||
payload = self._build_payload({
|
||||
"db": db_name,
|
||||
"login": "demo",
|
||||
"password": "admin",
|
||||
"context": {},
|
||||
})
|
||||
response = self.url_open("/web/session/authenticate", data=json.dumps(payload), headers=self.headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self._is_error_json_response(data)
|
||||
error = data["error"]
|
||||
self.assertEqual(error["code"], 200)
|
||||
self.assertEqual(error["message"], "Odoo Server Error")
|
||||
self.assertEqual(error["data"]["name"], "psycopg2.OperationalError")
|
||||
self.assertEqual(
|
||||
error["data"]["message"],
|
||||
'FATAL: database "%s" does not exist\n' % db_name,
|
||||
)
|
||||
|
||||
def test_avatar(self):
|
||||
"""
|
||||
This request is used to retrieve the user's picture
|
||||
"""
|
||||
self.authenticate("demo", "demo")
|
||||
response = self.url_open("/web/image?model=res.users&field=image_medium&id=%s" % self.session.uid)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
avatar = Image.open(BytesIO(response.content))
|
||||
self.assertIsInstance(avatar, Image.Image)
|
||||
|
||||
def test_session_info(self):
|
||||
"""
|
||||
This request is used to authenticate a user using its session id
|
||||
"""
|
||||
payload = self._build_payload()
|
||||
self.authenticate("demo", "demo")
|
||||
response = self.url_open("/web/session/get_session_info", data=json.dumps(payload), headers=self.headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self._is_success_json_response(data)
|
||||
result = data["result"]
|
||||
self.assertEqual(result["username"], "demo")
|
||||
self.assertEqual(result["db"], self.env.cr.dbname)
|
||||
self.assertEqual(result["uid"], self.session.uid)
|
||||
|
||||
def _build_payload(self, params={}):
|
||||
"""
|
||||
Helper to properly build jsonrpc payload
|
||||
"""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"id": str(uuid4()),
|
||||
"params": params,
|
||||
}
|
||||
|
||||
def _is_success_json_response(self, data):
|
||||
""""
|
||||
Helper to validate a standard JSONRPC response's structure
|
||||
"""
|
||||
self.assertEqual(list(data.keys()), ["jsonrpc", "id", "result"], "should be a valid jsonrpc response")
|
||||
self.assertTrue(isinstance(data["jsonrpc"], str))
|
||||
self.assertTrue(isinstance(data["id"], str))
|
||||
|
||||
def _is_error_json_response(self, data):
|
||||
"""
|
||||
Helper to validate an error JSONRPC response's structure
|
||||
"""
|
||||
self.assertEqual(list(data.keys()), ["jsonrpc", "id", "error"], "should be a valid error jsonrpc response")
|
||||
self.assertTrue(isinstance(data["jsonrpc"], str))
|
||||
self.assertTrue(isinstance(data["id"], str))
|
||||
self.assertTrue(isinstance(data["error"], dict))
|
||||
self.assertEqual(list(data["error"].keys()), ["code", "message", "data"], "should be a valid error structure")
|
||||
error = data["error"]
|
||||
self.assertTrue(isinstance(error["data"], dict))
|
||||
self.assertIn("name", error["data"])
|
||||
self.assertIn("message", error["data"])
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" inherit_id="web.assets_backend" name="Mobile Assets" priority="1">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/core/mixins.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/core/dialog.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/core/session.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/services/core.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/action_manager.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/barcode_fields.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/contact_sync.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/crash_manager.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/datepicker.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/form_renderer.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/form_view.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/pivot_renderer.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/user_menu.js"></script>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/src/js/user_preferences_form_view.js"></script>
|
||||
<link rel="stylesheet" type="text/scss" href="/odex25_web_mobile/static/src/scss/mobile.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/odex25_web_mobile/static/src/scss/pivot_view_mobile.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="tests_assets" name="mobile_tests_assets" inherit_id="web.tests_assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/tests/helpers/test_utils.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" name="odex25_web_mobile_tests" inherit_id="web.qunit_mobile_suite_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/tests/odex25_web_mobile_tests.js"/>
|
||||
<script type="text/javascript" src="/odex25_web_mobile/static/tests/odex25_barcodes_mobile_tests.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record model="ir.ui.view" id="partner_view_mobile_sync_button">
|
||||
<field name="name">partner.view.contact.button</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<contactsync> </contactsync>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_users_form_simple_modif" model="ir.ui.view">
|
||||
<field name="name">res.users.preferences.form.mobile</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="attributes">
|
||||
<attribute name="js_class">res_users_preferences_form</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue