合并企业版代码(未测试,先提交到测试分支)

This commit is contained in:
qihao.gong@jikimo.com
2023-04-14 17:42:23 +08:00
parent 7a7b3d7126
commit d28525526a
1300 changed files with 513579 additions and 5426 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,48 @@
/** @odoo-module */
import { device } from "web.config";
import * as LegacyControlPanel from "web.ControlPanel";
import { useBackButton } from "web_mobile.hooks";
import { patch } from "@web/core/utils/patch";
import { ControlPanel } from "@web/search/control_panel/control_panel";
if (device.isMobile) {
patch(LegacyControlPanel.prototype, "web_mobile", {
setup() {
this._super(...arguments);
useBackButton(this._onBackButton.bind(this), () => this.state.showMobileSearch);
},
//---------------------------------------------------------------------
// Handlers
//---------------------------------------------------------------------
/**
* close mobile search on back-button
* @private
*/
_onBackButton() {
this._resetSearchState();
},
});
patch(ControlPanel.prototype, "web_mobile", {
setup() {
this._super(...arguments);
useBackButton(this._onBackButton.bind(this), () => this.state.showMobileSearch);
},
//---------------------------------------------------------------------
// Handlers
//---------------------------------------------------------------------
/**
* close mobile search on back-button
* @private
*/
_onBackButton() {
this.resetSearchState();
},
});
}

View File

@@ -0,0 +1,79 @@
odoo.define("web_mobile.Dialog", function (require) {
"use strict";
var Dialog = require("web.Dialog");
var mobileMixins = require("web_mobile.mixins");
Dialog.include(
_.extend({}, mobileMixins.BackButtonEventMixin, {
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Close the current dialog on 'backbutton' event.
*
* @private
* @override
* @param {Event} ev
*/
_onBackButton: function () {
this.close();
},
})
);
});
odoo.define("web_mobile.OwlDialog", function (require) {
"use strict";
const OwlDialog = require("web.OwlDialog");
const { useBackButton } = require("web_mobile.hooks");
const { patch } = require("web.utils");
patch(OwlDialog.prototype, "web_mobile", {
setup() {
this._super(...arguments);
useBackButton(this._onBackButton.bind(this));
},
//---------------------------------------------------------------------
// Handlers
//---------------------------------------------------------------------
/**
* Close dialog on back-button
* @private
*/
_onBackButton() {
this._close();
},
});
});
odoo.define("web_mobile.Popover", function (require) {
"use strict";
const Popover = require("web.Popover");
const { useBackButton } = require("web_mobile.hooks");
const { patch } = require("web.utils");
patch(Popover.prototype, "web_mobile", {
setup() {
this._super(...arguments);
useBackButton(this._onBackButton.bind(this), () => this.state.displayed);
},
//---------------------------------------------------------------------
// Handlers
//---------------------------------------------------------------------
/**
* Close popover on back-button
* @private
*/
_onBackButton() {
this._close();
},
});
});

View File

@@ -0,0 +1,145 @@
odoo.define('web_mobile.mixins', function (require) {
"use strict";
const session = require('web.session');
const mobile = require('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 save() {
const changedFields = await this._super(...arguments);
await 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('web_mobile.hooks', function (require) {
"use strict";
const { backButtonManager } = require('web_mobile.core');
const { onMounted, onPatched, onWillUnmount, useComponent } = owl;
/**
* 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 is either enabled on mount or, using the `shouldEnable` function
* argument as condition, when the component is patched. In both cases,
* the feature is automatically disabled on unmount.
*
* @param {function} func the function to execute when the back button is
* pressed. The function is called with the custom event as param.
* @param {function} [shouldEnable] the function to execute when the DOM is
* patched to check if the backbutton should be enabled or disabled ;
* if undefined will be enabled on mount and disabled on unmount.
*/
function useBackButton(func, shouldEnable) {
const component = useComponent();
let isEnabled = false;
/**
* Enables the func listener, overriding default back button behavior.
*/
function enable() {
backButtonManager.addListener(component, func);
isEnabled = true;
}
/**
* Disables the func listener, restoring the default back button behavior if
* no other listeners are present.
*/
function disable() {
backButtonManager.removeListener(component);
isEnabled = false;
}
onMounted(() => {
if (shouldEnable && !shouldEnable()) {
return;
}
enable();
});
onPatched(() => {
if (!shouldEnable) {
return;
}
const shouldBeEnabled = shouldEnable();
if (shouldBeEnabled && !isEnabled) {
enable();
} else if (!shouldBeEnabled && isEnabled) {
disable();
}
});
onWillUnmount(() => {
if (isEnabled) {
disable();
}
});
}
return {
useBackButton,
};
});

View File

@@ -0,0 +1,18 @@
/** @odoo-module **/
import mobile from "web_mobile.core";
import { download } from "@web/core/network/download";
const _download = download._download;
download._download = async function (options) {
if (mobile.methods.downloadFile) {
if (odoo.csrf_token) {
options.csrf_token = odoo.csrf_token;
}
mobile.methods.downloadFile(options);
return Promise.resolve();
} else {
return _download.apply(this, arguments);
}
};

View File

@@ -0,0 +1,89 @@
odoo.define('web_mobile.Session', function (require) {
"use strict";
const core = require('web.core');
const Session = require('web.Session');
const mobile = require('web_mobile.core');
const DEFAULT_AVATAR_SIZE = 128;
/*
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 base64Avatar = await this.fetchAvatar();
return mobile.methods.updateAccount({
avatar: base64Avatar.substring(base64Avatar.indexOf(',') + 1),
name: this.name,
username: this.username,
});
},
/**
* Fetch current user's avatar as PNG image
*
* @returns {Promise} resolved with the dataURL, or rejected if the file is
* empty or if an error occurs.
*/
fetchAvatar() {
const url = this.url('/web/image', {
model: 'res.users',
field: 'image_medium',
id: this.uid,
});
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
canvas.width = DEFAULT_AVATAR_SIZE;
canvas.height = DEFAULT_AVATAR_SIZE;
const context = canvas.getContext('2d');
const image = new Image();
image.addEventListener('load', () => {
context.drawImage(image, 0, 0, DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE);
resolve(canvas.toDataURL('image/png'));
});
image.addEventListener('error', reject);
image.src = url;
});
},
});
});

View File

@@ -0,0 +1,14 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import mobile from "web_mobile.core";
function mobileErrorHandler(env, error, originalError) {
if (mobile.methods.crashManager) {
error.originalError = originalError;
mobile.methods.crashManager(error);
}
}
registry
.category("error_handlers")
.add("web_mobile.errorHandler", mobileErrorHandler, { sequence: 3 });

View File

@@ -0,0 +1,26 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import mobile from "web_mobile.core";
import { shortcutItem, switchAccountItem } from "./user_menu_items";
const serviceRegistry = registry.category("services");
const userMenuRegistry = registry.category("user_menuitems");
const mobileService = {
start() {
if (mobile.methods.addHomeShortcut) {
userMenuRegistry.add("web_mobile.shortcut", shortcutItem);
}
if (mobile.methods.switchAccount) {
// remove "Log Out" and "My Odoo.com Account"
userMenuRegistry.remove('log_out');
userMenuRegistry.remove('odoo_account');
userMenuRegistry.add("web_mobile.switch", switchAccountItem);
}
},
};
serviceRegistry.add("mobile", mobileService);

View File

@@ -0,0 +1,158 @@
/* global OdooDeviceUtility */
odoo.define('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;
});
}
/**
* Error related to the registration of a listener to the backbutton event
*/
class BackButtonListenerError extends Error {}
/**
* 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
* @throws {BackButtonListenerError} if the listener has already been registered
*/
addListener(listener, func) {
if (!methods.overrideBackButton) {
return;
}
if (this._listeners.has(listener)) {
throw new BackButtonListenerError("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
* @throws {BackButtonListenerError} if the listener has already been unregistered
*/
removeListener(listener) {
if (!methods.overrideBackButton) {
return;
}
if (!this._listeners.has(listener)) {
throw new BackButtonListenerError("This listener has already been unregistered.");
}
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,
BackButtonListenerError,
backButtonManager,
methods,
};
});

View File

@@ -0,0 +1,38 @@
/** @odoo-module **/
import mobile from "web_mobile.core";
export function shortcutItem(env) {
return {
type: "item",
id: "web_mobile.shortcut",
description: env._t("Add to Home Screen"),
callback: () => {
const { hash } = env.services.router.current;
if (hash.menu_id) {
const menu = env.services.menu.getMenu(hash.menu_id);
const base64Icon = menu && menu.webIconData;
mobile.methods.addHomeShortcut({
title: document.title,
shortcut_url: document.URL,
web_icon: base64Icon.substring(base64Icon.indexOf(',') + 1),
});
} else {
env.services.notification.add(env._t("No shortcut for Home Menu"));
}
},
sequence: 100,
};
}
export function switchAccountItem(env) {
return {
type: "item",
id: "web_mobile.switch",
description: env._t("Switch/Add Account"),
callback: () => {
mobile.methods.switchAccount();
},
sequence: 100,
};
}

View File

@@ -0,0 +1,23 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { patch } from "@web/core/utils/patch";
import { formView } from "@web/views/form/form_view";
import { UpdateDeviceAccountControllerMixin } from "web_mobile.mixins";
import { Record, RelationalModel } from "@web/views/basic_relational_model";
export class ResUsersPreferenceRecord extends Record {}
export class ResUsersPreferenceModel extends RelationalModel {}
ResUsersPreferenceModel.Record = ResUsersPreferenceRecord;
patch(
ResUsersPreferenceRecord.prototype,
"res_users_controller_mobile_mixin",
UpdateDeviceAccountControllerMixin
);
registry.category("views").add("res_users_preferences_form", {
...formView,
Model: ResUsersPreferenceModel,
});