Skip to Content
Odoo เมนู
  • ลงชื่อเข้าใช้
  • ทดลองใช้ฟรี
  • แอป
    การเงิน
    • ระบบบัญชี
    • ระบบการออกใบแจ้งหนี้
    • ระบบบัญชีรายจ่าย
    • ระบบการจัดการข้อมูลผ่านสเปรดชีต (BI)
    • เอกสาร
    • ระบบลงลายเซ็น
    การขาย
    • ลูกค้าสัมพันธ์
    • การขาย
    • POS ร้านค้า
    • POS ร้านอาหาร
    • ระบบสมัครสมาชิก
    • การเช่า
    การจัดการเว็บไซต์
    • เครื่องมือสร้างเว็บไซต์
    • ระบบการขายออนไลน์
    • บล็อก
    • ฟอรั่ม
    • ไลฟ์แชท
    • ระบบการอบรมออนไลน์
    ซัพพลายเชน
    • คลังสินค้า
    • ระบบการผลิต
    • PLM
    • ระบบการจัดซื้อ
    • การซ่อมบำรุง
    • ระบบจัดการคุณภาพ
    ทรัพยากรบุคคล
    • ข้อมูลพนักงาน
    • ระบบสรรหาบุคลากร
    • ระบบการลา
    • การประเมิน
    • ระบบการแนะนำพนักงาน
    • ระบบจัดการยานพาหนะ
    การตลาด
    • โซเชียลมาร์เก็ตติ้ง
    • การตลาดผ่านอีเมล
    • ระบบการตลาดผ่าน SMS
    • ระบบการจัดงานอีเวนต์
    • การตลาดแบบอัตโนมัติ
    • ระบบแบบสำรวจ
    การบริการ
    • โปรเจกต์
    • ระบบบันทึกเวลา
    • ระบบการจัดการบริการภาคสนาม
    • ระบบดูแลช่วยเหลือ
    • การวางแผน
    • การนัดหมาย
    ประสิทธิผล
    • ระบบแสดงความคิดเห็น
    • ระบบการอนุมัติ
    • ไอโอที
    • VoIP
    • คลังข้อมูล
    • WhatsApp
    แอปพลิเคชันของบุคคลที่สาม Odoo สตูดิโอ แพลตฟอร์มคลาวด์ของ Odoo
  • อุตสาหกรรม
    การค้าปลีก
    • ร้านขายหนังสือ
    • ร้านขายเสื้อผ้า
    • ร้านขายเฟอร์นิเจอร์
    • ร้านขายของชำ
    • ร้านขายฮาร์ดแวร์
    • ร้านขายของเล่น
    Food & Hospitality
    • บาร์และผับ
    • ร้านอาหาร
    • อาหารฟาสต์ฟู้ด
    • Guest House
    • ตัวแทนจำหน่ายเครื่องดื่ม
    • Hotel
    อสังหาริมทรัพย์
    • Real Estate Agency
    • บริษัทสถาปัตยกรรม
    • บริษัทรับเหมา
    • การจัดการอสังหาริมทรัพย์
    • การจัดสวน
    • สมาคมเจ้าของอสังหาริมทรัพย์
    ปรึกษา
    • สำนักงานบัญชี
    • Odoo พาร์ทเนอร์
    • เอเจนซี่การตลาด
    • สำนักงานกฎหมาย
    • บริการสรรหาบุคลากร
    • การตรวจสอบและการรับรอง
    ระบบการผลิต
    • Textile
    • เหล็ก
    • เฟอร์นิเจอร์
    • อาหาร
    • Brewery
    • ของขวัญขององค์กร
    สุขภาพ & ฟิตเนส
    • ชมรมกีฬา
    • ร้านขายแว่นตา
    • ฟิตเนส
    • ผู้ประกอบวิชาชีพด้านสุขภาพ
    • ร้านขายยา
    • ร้านทำผม
    Trades
    • ช่างซ่อม
    • ฮาร์ดแวร์และการสนับสนุนด้านไอที
    • ระบบพลังงานแสงอาทิตย์
    • ช่างทำรองเท้า
    • บริการทำความสะอาด
    • HVAC Services
    อื่น ๆ
    • Nonprofit Organization
    • สำนักงานสิ่งแวดล้อม
    • ร้านเช่าป้ายโฆษณา
    • การถ่ายภาพ
    • ร้านเช่าจักรยาน
    • ตัวแทนจำหน่ายซอฟต์แวร์
    Browse all Industries
  • คอมมูนิตี้
    ศึกษา
    • บทเรียน
    • เอกสารกำกับโปรแกรม
    • การรับรอง
    • การฝึกอบรม
    • บล็อก
    • พอดแคสต์
    เพิ่มศักยภาพให้กับการศึกษา
    • โปรแกรมการศึกษา
    • Scale Up! Business Game
    • เยี่ยมชม Odoo
    รับซอฟต์แวร์
    • ดาวน์โหลด
    • เปรียบเทียบรุ่น
    • เผยแพร่
    ทำงานร่วมกัน
    • Github
    • ฟอรั่ม
    • ระบบการจัดงานอีเวนต์
    • การแปล
    • ร่วมเป็นพาร์ทเนอร์
    • Services for Partners
    • ลงทะเบียนเพื่อสร้างบัญชีบริษัทของคุณ
    รับบริการ
    • ค้นหาพาร์ทเนอร์
    • ค้นหานักบัญชี
    • พบกับที่ปรึกษา
    • บริการติดตั้งระบบเพื่อใช้งาน
    • ข้อมูลอ้างอิงลูกค้า
    • การสนับสนุน
    • อัปเกรด
    Github Youtube Twitter Linkedin Instagram Facebook Spotify
    +1 (650) 691-3277
    รับการสาธิต
  • ราคา
  • ช่วยเหลือ

Odoo is the world's easiest all-in-one management software.
It includes hundreds of business apps:

  • ลูกค้าสัมพันธ์
  • e-Commerce
  • ระบบบัญชี
  • สินค้าคงคลัง
  • PoS
  • โปรเจกต์
  • MRP
All apps
คุณต้องลงทะเบียนเพื่อโต้ตอบกับคอมมูนิตี้
All Posts คน เหรียญรางวัล
แท็ก (View all)
odoo accounting v14 pos v15
เกี่ยวกับฟอรั่มนี้
คุณต้องลงทะเบียนเพื่อโต้ตอบกับคอมมูนิตี้
All Posts คน เหรียญรางวัล
แท็ก (View all)
odoo accounting v14 pos v15
เกี่ยวกับฟอรั่มนี้
ช่วยเหลือ

Migrating Javascript from Odoo 15 to Odoo 17.

ติดตาม

รับการแจ้งเตือนเมื่อมีกิจกรรมในโพสต์นี้

คำถามนี้ถูกตั้งค่าสถานะ
odoo1715
1 ตอบกลับ
4672 มุมมอง
อวตาร
Jay

I am getting errors while migrating this code to odoo 17:
getting this error: 
The following modules are needed by other modules but have not been defined, they may not be present in the correct asset bundle: @web/core/actions/action @web/views/view_registry The following modules could not be loaded because they have unmet dependencies, this is a secondary error which is likely caused by one of the above problems: @stock_3d_view/js/form_3d_view @stock_3d_view/js/listview_3d


Original code:

odoo.define('button_near_create.tree_button', function(require) {
"use strict";
var ListController = require('web.ListController');
var ListView = require('web.ListView');
var viewRegistry = require('web.view_registry');
var core = require('web.core');
var session = require('web.session');
var Dialog = require('web.Dialog');
var QWeb = core.qweb;
var rpc = require('web.rpc');
var ajax = require('web.ajax');
var PositionDialog = Dialog.extend({
/**
* Initialize the PositionDialog.
*
* @param {Object} parent - The parent object.
* @param {Object} options - Dialog options.
* @param {Object} pointer - Object containing x and y coordinates.
* @param {Function} close - Function to be called on dialog close.
*/
init: function(parent, options) {
var opt = options;
this._super(...arguments)
this.pointer = opt.pointer
this.onClickClose = opt.close
},
/**
* Render the PositionDialog element.
* Set the dialog's position based on the provided coordinates.
*/
renderElement: function() {
this._super()
this.$modal.find('.modal-dialog').css({
position: 'absolute',
left: this.pointer.x,
top: this.pointer.y,
})
}
})
//Extends list controller class to add event listener for 3D button.
var TreeButton = ListController.extend({
buttons_template: 'StockLocationListView.buttons',
events: _.extend({}, ListController.prototype.events, {
'click .open_3d_view': '_Open3DView',
}),
/**
* Starts the process of displaying rendered 3D object of stock.location.
*/
_Open3DView: async function(ev) {
var self = this;
var wh_data;
var data;
var loc_quant;
let controls, renderer, clock, scene, camera, pointer, raycaster;
var mesh, group;
var material;
var loc_color;
var loc_opacity = 0.5;
var textSize;
let selectedObject = null;
var dialogs = null;
var wh_id;
/**
* Make a jsonRpc call to fetch available warehouses and list it in the dropdown.
*
* @await
* @param {integer} company_id
*/
await ajax.jsonRpc('/3Dstock/warehouse', 'call', {
'company_id': session.user_context.allowed_company_ids[0],
}).then(function(incoming_data) {
wh_data = incoming_data;
});
wh_id = wh_data[0][0];
var select = document.createElement("select");
select.name = "mySelect";
for (let i = 0; i < wh_data.length; i++) {
var option = document.createElement("option");
option.value = wh_data[i][0];
option.text = wh_data[i][1];
select.appendChild(option);
select.classList.add("customselect");
}

var closeDiv = document.createElement("button");
closeDiv.classList.add("closeBtn");
closeDiv.innerHTML = "&times;"
var colorDiv = document.createElement("div");
colorDiv.classList.add("rectangle");
var color1 = document.createElement("div");
color1.classList.add("square1");
colorDiv.appendChild(color1);
var colorText1 = document.createElement("div");
colorText1.classList.add("squareText1");
colorText1.innerHTML = "Overload";
colorDiv.appendChild(colorText1);
var color2 = document.createElement("div");
color2.classList.add("square2");
colorDiv.appendChild(color2);
var colorText2 = document.createElement("div");
colorText2.classList.add("squareText2");
colorText2.innerHTML = "Almost Full";
colorDiv.appendChild(colorText2);
var color3 = document.createElement("div");
color3.classList.add("square3");
colorDiv.appendChild(color3);
var colorText3 = document.createElement("div");
colorText3.classList.add("squareText3");
colorText3.innerHTML = "Free Space Available";
colorDiv.appendChild(colorText3);
var color4 = document.createElement("div");
color4.classList.add("square4");
colorDiv.appendChild(color4);
var colorText4 = document.createElement("div");
colorText4.classList.add("squareText4");
colorText4.innerHTML = "No Product/Load";
colorDiv.appendChild(colorText4);

start();
/**
* The complete working of fetching data from stock.location and displaying them in the form of 3d objects.
*
* @async
*/
async function start() {
/**
* Make a jsonRpc call to fetch location details of corresponding warehouse.
*
* @await
* @param {integer} company_id
* @param {integer} wh_id
*/
await ajax.jsonRpc('/3Dstock/data', 'call', {
'company_id': session.user_context.allowed_company_ids[0],
'wh_id': wh_id,
}).then(function(incoming_data) {
data = incoming_data;
});
scene = new THREE.Scene();
scene.background = new THREE.Color(0xdfdfdf);
clock = new THREE.Clock();
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.5, 6000);
camera.position.set(0, 200, 300)
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight / 1.164);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.render(scene, camera);
self.$el.find('.o_content').html(renderer.domElement);
self.$el.find('.o_content').append(select);
self.$el.find('.o_content').append(colorDiv);
self.$el.find('.o_content').append(closeDiv);
var dropdown = document.querySelector(".customselect");
if (dropdown) {
dropdown.addEventListener("change", warehouseChange);
}
var closeBtn = document.querySelector(".closeBtn");
if (closeBtn) {
closeBtn.addEventListener("click", onWindowClose);
}
controls = new THREE.OrbitControls(camera, renderer.domElement);
const baseGeometry = new THREE.BoxGeometry(800, 0, 800);
const baseMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: false,
opacity: 1,
side: THREE.BackSide,
});
const baseCube = new THREE.Mesh(baseGeometry, baseMaterial);
scene.add(baseCube);
group = new THREE.Group();
//traversing through each location object
for (let [key, value] of Object.entries(data)) {
//checks if the dimension values of the location are non zero
if ((value[0] > 0) || (value[1] > 0) || (value[2] > 0) || (value[3] > 0) || (value[4] > 0) || (value[5] > 0)) {
const geometry = new THREE.BoxGeometry(value[3], value[5], value[4]);
geometry.translate(0, (value[5] / 2), 0);
const edges = new THREE.EdgesGeometry(geometry);
/**
* Make a jsonRpc call to fetch the stock details of particular location.
*
* @await
* @param {integer} loc_code
*/
await ajax.jsonRpc('/3Dstock/data/quantity', 'call', {
'loc_code': key,
}).then(function(quant_data) {
loc_quant = quant_data;
});
//checking the quantity and capacity of the location to determine the color of the location
if (loc_quant[0] > 0) {
if (loc_quant[1] > 100) {
loc_color = 0xcc0000;
loc_opacity = 0.8;
} else if (loc_quant[1] > 50) {
loc_color = 0xe6b800;
loc_opacity = 0.8;
} else {
loc_color = 0x00802b;
loc_opacity = 0.8;
}
} else {
if (loc_quant[1] == -1) {
loc_color = 0x00802b;
loc_opacity = 0.8;
} else {
loc_color = 0x8c8c8c;
loc_opacity = 0.5;
}
}
//creating a 3D box geometry for each location
material = new THREE.MeshBasicMaterial({
color: loc_color,
transparent: true,
opacity: loc_opacity
});
mesh = new THREE.Mesh(geometry, material);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({
color: 0x404040
}));
line.position.x = value[0];
line.position.y = value[1];
line.position.z = value[2];
mesh.position.x = value[0];
mesh.position.y = value[1];
mesh.position.z = value[2];
const loader = new THREE.FontLoader();
loader.load('https://threejs.org/examples/fonts/droid/droid_sans_bold.typeface.json', function(font) {
const textcolor = 0x000000;
const textMat = new THREE.MeshBasicMaterial({
color: textcolor,
side: THREE.DoubleSide,
});
const textmessage = key;
if (value[3] > value[4]) {
textSize = (value[4] / 2) - (value[4] / 2.9);
} else {
textSize = (value[3] / 2) - (value[3] / 2.9);
}
const textshapes = font.generateShapes(textmessage, textSize);
const textgeometry = new THREE.ShapeGeometry(textshapes);
textgeometry.translate(0, ((value[5] / 2) - (textSize / (textSize - 1.5))), 0);
const text = new THREE.Mesh(textgeometry, textMat);
if (value[4] > value[3]) {
text.rotation.y = Math.PI / 2;
text.position.x = value[0];
text.position.y = value[1];
text.position.z = value[2] + (textSize * 2) + ((value[3] / 3.779 / 2) / 2) + (textSize / 2);
} else {
text.position.x = value[0] - (textSize * 2) - ((value[4] / 3.779 / 2) / 2) - (textSize / 2);
text.position.y = value[1];
text.position.z = value[2];
}
scene.add(text);
});
scene.add(mesh);
scene.add(line);
mesh.name = key;
mesh.userData = {
color: loc_color
};
group.add(mesh);
}
}
scene.add(group);
raycaster = new THREE.Raycaster();
pointer = new THREE.Vector3();

animate();
}
/**
* Triggered when users change warehouse and calls the start() function with the latest warehouse id.
*/
function warehouseChange() {
var selectedBox = document.querySelector(".customselect");
var selectValue = selectedBox.value;
wh_id = selectValue;
start();
}
/**
* Handles the resizing and setting the pixel ration on window resize.
*/
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight / 1.164);
}
/**
* Triggered when user clicks on close button.
*/
function onWindowClose() {
window.location.reload();
}
/**
* Animates and renders the three.renderer object to make changes on the scene.
*/
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
renderer.render(scene, camera);
var canvas = document.getElementsByTagName("canvas")[0];
var selectedBox = document.querySelector(".customselect");
var colorBox = document.querySelector(".rectangle");
var closeDiv = document.querySelector(".closeBtn");
//checking the canvas element adding event listener on canvas.
if (canvas == null) {
window.removeEventListener('dblclick', onPointerMove);
window.removeEventListener('resize', onWindowResize);
if (selectedBox) {
selectedBox.style.display = "none";
}
if (colorBox) {
colorBox.style.display = "none";
}
if (closeDiv) {
closeDiv.style.display = "none";
}
} else {
window.addEventListener('dblclick', onPointerMove);
window.addEventListener('resize', onWindowResize);
if (selectedBox) {
selectedBox.style.display = "block";
}
if (colorBox) {
colorBox.style.display = "block";
}
if (closeDiv) {
closeDiv.style.display = "block";
}
}
}
/**
* gets the coordinates of the mouse point and checks for any objects.
*
* @async
* @param {object} event
*/
async function onPointerMove(event) {
var products;
var button;
if (dialogs == null) {
if (selectedObject) {
selectedObject.material.color.set(selectedObject.userData.color);
selectedObject = null;
} else {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / (window.innerHeight)) * 2 + 1 + 0.13;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObject(group, true);
if (intersects.length > 0) {
const res = intersects.filter(function(res) {
return res && res.object;
})[0];
if (res && res.object) {
/**
* Make a jsonRpc call to fetch the details of products and their quantity of selected location.
*
* @await
* @param {integer} loc_code
*/
await ajax.jsonRpc('/3Dstock/data/product', 'call', {
'loc_code': res.object.name,
}).then(function(product_data) {
products = product_data;
});
selectedObject = res.object;
selectedObject.material.color.set(0x00ffcc);

function onClickClose() {
if (selectedObject) {
selectedObject.material.color.set(selectedObject.userData.color);
selectedObject = null;
dialogs.close();
dialogs = null;
}
}
//opens a new dialogbox with proeduct and quantity details
dialogs = new PositionDialog(this, {
title: ('Location: ' + res.object.name),
size: 'small',
$content: $(QWeb.render('ViewLocationData', {
data: products,
})),
placement: 'bottom',
renderFooter: false,
pointer: {
x: event.clientX,
y: event.clientY,
},
close: onClickClose,
}).open();

if (dialogs) {
window.addEventListener('click', onClickClose);
} else {
window.removeEventListener('click', onClickClose);
}
}
}
}
}
}
}
});
var StockLocationTreeView = ListView.extend({
config: _.extend({}, ListView.prototype.config, {
Controller: TreeButton,
}),
});
viewRegistry.add('3d_button_in_stock', StockLocationTreeView);
});


odoo.define('stock_3d_view.action_open_form_3d_view', function(require) {
'use strict';
var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var rpc = require('web.rpc');
var QWeb = core.qweb;
var Dialog = require('web.Dialog');
var ajax = require('web.ajax');
var PositionDialog = Dialog.extend({
/**
* Initializes the PositionDialog instance.
*
* @constructor
* @param {Object} parent - The parent object.
* @param {Object} options - Configuration options.
* @param {Object} options.pointer - The pointer object.
* @param {Function} options.close - Callback function for closing the dialog.

no of users
-
*/
init: function(parent, options) {
var opt = options;
this._super(...arguments)
this.pointer = opt.pointer
this.onClickClose = opt.close
},
/**
* Renders the element and sets its position.
*
*/
renderElement: function() {
this._super()
this.$modal.find('.modal-dialog').css({
position: 'absolute',
left: this.pointer.x,
top: this.pointer.y,
})
}
})
//Extends abstract action class to add event listener for 3D button.
var open_form_3d_view = AbstractAction.extend({
template: 'Location3DFormView',
events: _.extend({}, AbstractAction.prototype.events, {
'click .breadcrumb-item a': 'onBreadcrumbClick'
}),
/**
* Handles the click event on breadcrumbs.
*
* @param {Event} ev - The click event object.
*/
onBreadcrumbClick: function(ev) {
let jsId = this.$(ev.target).attr('jsId');
this.actionService.restore(jsId);
},
/**
* Initializes the open_form_3d_view instance.
*
* @constructor
* @param {Object} parent - The parent object.
* @param {Object} action - The action configuration.
*/
init: function(parent, action) {
this._super(...arguments)
this.breadcrumbs = parent.wowlEnv.config.breadcrumbs
this.actionService = parent.actionService;
},
/**
* Starts the open_form_3d_view action.
*/
start: function() {
this.Open3DView();
},
/**
* Starts the process of displaying rendered 3D object of stock.location.
*/
Open3DView: function() {
var self = this;
var wh_data;
var data;
var loc_quant;
let controls, renderer, clock, scene, camera, pointer, raycaster;
var mesh, group;
var material;
var loc_color;
var loc_opacity = 0.5;
var textSize;
let selectedObject = null;
var dialogs = null;
var wh_id;
var location_id = self.searchModel.config.context.loc_id || localStorage.getItem("location_id");
//sets location_id and company_id in local storage
if (self.searchModel.config.context.loc_id != null) {
localStorage.setItem("location_id", self.searchModel.config.context.loc_id);
localStorage.setItem("company_id", self.searchModel.config.context.company_id);
}

var colorDiv = document.createElement("div");
colorDiv.classList.add("rectangle");
var color1 = document.createElement("div");
color1.classList.add("square1");
colorDiv.appendChild(color1);
var colorText1 = document.createElement("div");
colorText1.classList.add("squareText1");
colorText1.innerHTML = "Overload";
colorDiv.appendChild(colorText1);
var color2 = document.createElement("div");
color2.classList.add("square2");
colorDiv.appendChild(color2);
var colorText2 = document.createElement("div");
colorText2.classList.add("squareText2");
colorText2.innerHTML = "Almost Full";
colorDiv.appendChild(colorText2);
var color3 = document.createElement("div");
color3.classList.add("square3");
colorDiv.appendChild(color3);
var colorText3 = document.createElement("div");
colorText3.classList.add("squareText3");
colorText3.innerHTML = "Free Space Available";
colorDiv.appendChild(colorText3);
var color4 = document.createElement("div");
color4.classList.add("square4blue");
colorDiv.appendChild(color4);
var colorText4 = document.createElement("div");
colorText4.classList.add("squareText4");
colorText4.innerHTML = "No Product/Load";
colorDiv.appendChild(colorText4);

start();
/**
* The complete working of fetching data from stock.location and displaying them in the form of 3d objects.
*
* @async
*/
async function start() {
/**
* Make a jsonRpc call to fetch location details.
*
* @await
* @param {integer} company_id
* @param {integer} loc_id
*/
await ajax.jsonRpc('/3Dstock/data/standalone', 'call', {
'company_id': self.searchModel.config.context.company_id || localStorage.getItem("company_id"),
'loc_id': self.searchModel.config.context.loc_id || localStorage.getItem("location_id"),
}).then(function(incoming_data) {
data = incoming_data;
});
//creating a new three.scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0xdfdfdf);
clock = new THREE.Clock();
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.5, 6000);
camera.position.set(0, 200, 300)
renderer = new THREE.WebGLRenderer({
antialias: true
});
//setting size and pixel ratio for the renderer.
renderer.setSize(window.innerWidth, window.innerHeight / 1.163);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.render(scene, camera);
var o_content = self.$('.o_content')
o_content.append(renderer.domElement);
self.$el.find('.o_content').append(colorDiv);
controls = new THREE.OrbitControls(camera, renderer.domElement);
const baseGeometry = new THREE.BoxGeometry(800, 0, 800);
const baseMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: false,
opacity: 1,
side: THREE.FrontSide,
});
const baseCube = new THREE.Mesh(baseGeometry, baseMaterial);
scene.add(baseCube);
group = new THREE.Group();
//traversing through each location object
for (let [key, value] of Object.entries(data)) {
//checks if the dimension values of the location are non zero
if ((value[0] > 0) || (value[1] > 0) || (value[2] > 0) || (value[3] > 0) || (value[4] > 0) || (value[5] > 0)) {
const geometry = new THREE.BoxGeometry(value[3], value[5], value[4]);
geometry.translate(0, (value[5] / 2), 0);
const edges = new THREE.EdgesGeometry(geometry);
/**
* Make a jsonRpc call to fetch the stock details of particular location.
*
* @await
* @param {integer} loc_code
*/
await ajax.jsonRpc('/3Dstock/data/quantity', 'call', {
'loc_code': key,
}).then(function(quant_data) {
loc_quant = quant_data;
});
//checking the quantity and capacity of the location to determine the color of the location
if (localStorage.getItem("location_id") == value[6]) {
if (loc_quant[0] > 0) {
if (loc_quant[1] > 100) {
loc_color = 0xcc0000;
loc_opacity = 0.8;
} else if (loc_quant[1] > 50) {
loc_color = 0xe6b800;
loc_opacity = 0.8;
} else {
loc_color = 0x00802b;
loc_opacity = 0.8;
}
} else {
if (loc_quant[1] == -1) {
loc_color = 0x00802b;
loc_opacity = 0.8;
} else {
loc_color = 0x0066ff;
loc_opacity = 0.8;
}
}
} else {
loc_color = 0x8c8c8c;
loc_opacity = 0.5;
}
//creating a 3D box geometry for each location
material = new THREE.MeshBasicMaterial({
color: loc_color,
transparent: true,
opacity: loc_opacity
});
mesh = new THREE.Mesh(geometry, material);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({
color: 0x404040
}));
line.position.x = value[0];
line.position.y = value[1];
line.position.z = value[2];
mesh.position.x = value[0];
mesh.position.y = value[1];
mesh.position.z = value[2];
const loader = new THREE.FontLoader();
loader.load('https://threejs.org/examples/fonts/droid/droid_sans_bold.typeface.json', function(font) {
const textcolor = 0x000000;
const textMat = new THREE.MeshBasicMaterial({
color: textcolor,
side: THREE.DoubleSide,
});
const textmessage = key;
if (value[3] > value[4]) {
textSize = (value[4] / 2) - (value[4] / 2.9);
} else {
textSize = (value[3] / 2) - (value[3] / 2.9);
}
const textshapes = font.generateShapes(textmessage, textSize);
const textgeometry = new THREE.ShapeGeometry(textshapes);
textgeometry.translate(0, ((value[5] / 2) - (textSize / (textSize - 1.5))), 0);
const text = new THREE.Mesh(textgeometry, textMat);
if (value[4] > value[3]) {
text.rotation.y = Math.PI / 2;
text.position.x = value[0];
text.position.y = value[1];
text.position.z = value[2] + (textSize * 2) + ((value[3] / 3.779 / 2) / 2) + (textSize / 2);
} else {
text.position.x = value[0] - (textSize * 2) - ((value[4] / 3.779 / 2) / 2) - (textSize / 2);
text.position.y = value[1];
text.position.z = value[2];
}
scene.add(text);
});
scene.add(mesh);
scene.add(line);
mesh.name = key;
mesh.userData = {
color: loc_color,
loc_id: value[6],
};
group.add(mesh);
}
}
scene.add(group);
raycaster = new THREE.Raycaster();
pointer = new THREE.Vector3();
animate();
}
/**
* Handles the resizing and setting the pixel ration on window resize.
*/
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight / 1.163);
}
/**
* Animates and renders the three.renderer object to make changes on the scene.
*/
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
renderer.render(scene, camera);
var canvas = document.getElementsByTagName("canvas")[0];
var colorBox = document.querySelector(".rectangle");
//checking the canvas element adding event listener on canvas.
if (canvas == null) {
window.removeEventListener('dblclick', onPointerMove);
window.removeEventListener('resize', onWindowResize);
if (colorBox) {
colorBox.style.display = "none";
}
} else {
window.addEventListener('dblclick', onPointerMove);
window.addEventListener('resize', onWindowResize);
if (colorBox) {
colorBox.style.display = "block";
}
}
}
/**
* gets the coordinates of the mouse point and checks for any objects.
*
* @async
* @param {object} event
*/
async function onPointerMove(event) {
var products;
var button;
if (dialogs == null) {
if (selectedObject) {
selectedObject.material.color.set(selectedObject.userData.color);
selectedObject = null;
} else {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / (window.innerHeight)) * 2 + 1 + 0.13;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObject(group, true);
if (intersects.length > 0) {
const res = intersects.filter(function(res) {
return res && res.object;
})[0];
if (res && res.object) {
if (res.object.userData.loc_id == location_id) {
/**
* Make a jsonRpc call to fetch the details of products and their quantity of selected location.
*
* @await
* @param {integer} loc_code
*/
await ajax.jsonRpc('/3Dstock/data/product', 'call', {
'loc_code': res.object.name,
}).then(function(product_data) {
products = product_data;
});
selectedObject = res.object;
selectedObject.material.color.set(0x00ffcc);

function onClickClose() {
if (selectedObject) {
selectedObject.material.color.set(selectedObject.userData.color);
selectedObject = null;
dialogs.close();
dialogs = null;
}
}
//opens a new dialogbox with proeduct and quantity details
dialogs = new PositionDialog(this, {
title: ('Location: ' + res.object.name),
size: 'small',
$content: $(QWeb.render('ViewLocationData', {
data: products,
})),
placement: 'bottom',
renderFooter: false,
pointer: {
x: event.clientX,
y: event.clientY,
},
close: onClickClose,
}).open();

if (dialogs) {
window.addEventListener('click', onClickClose);
} else {
window.removeEventListener('click', onClickClose);
}
}
}
}
}
}
}
}

});
core.action_registry.add("open_form_3d_view", open_form_3d_view);
return open_form_3d_view;
});


My code:

/** @odoo-module **/

//import { registry } from '@web/core/registry';
import { Dialog } from '@web/core/dialog/dialog';
import { Component } from '@odoo/owl';
import ajax from 'web.ajax';
import * as THREE from 'three';
import { whenNextTick } from '@web/core/utils/timing';

class PositionDialog extends Dialog {
setup() {
super.setup();
this.pointer = this.props.pointer;
}
patched() {
super.patched();
// Adjust dialog position
const dialog = this.el.querySelector('.modal-dialog');
dialog.style.position = 'absolute';
dialog.style.left = `${this.pointer.x}px`;
dialog.style.top = `${this.pointer.y}px`;
}
}

class OpenForm3DView extends Component {
setup() {
super.setup();
this.actionService = this.env.services.action;
this.searchModel = this.env.searchModel;
this.companyId = this.env.searchModel.config.context.company_id;
this.locationId = this.env.searchModel.config.context.loc_id || localStorage.getItem("location_id");

if (this.locationId) {
localStorage.setItem("location_id", this.locationId);
localStorage.setItem("company_id", this.companyId);
}

this.breadcrumbs = this.env.config.breadcrumbs;
this.pointer = new THREE.Vector3();
this.dialog = null;
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xdfdfdf);
this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.5, 6000);
this.camera.position.set(0, 200, 300);
this.clock = new THREE.Clock();
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.raycaster = new THREE.Raycaster();
this.group = new THREE.Group();
}

async willStart() {
this._createColorLegend();
await this._fetchData();
}

mounted() {
this.renderer.setSize(window.innerWidth, window.innerHeight / 1.163);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.render(this.scene, this.camera);

this.el.appendChild(this.renderer.domElement);
this.el.appendChild(this.colorDiv);

this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this._createBaseCube();
this._createLocationObjects();
this._addEventListeners();
this._animate();
}

willUnmount() {
this._removeEventListeners();
}

// Event listener for breadcrumbs
onBreadcrumbClick(event) {
const jsId = event.target.getAttribute('jsId');
this.actionService.restore(jsId);
}

_addEventListeners() {
window.addEventListener('resize', this._onWindowResize.bind(this));
this.renderer.domElement.addEventListener('dblclick', this._onPointerMove.bind(this));
}

_removeEventListeners() {
window.removeEventListener('resize', this._onWindowResize.bind(this));
this.renderer.domElement.removeEventListener('dblclick', this._onPointerMove.bind(this));
}

async _fetchData() {
// Fetch location and stock data for 3D rendering
const locationData = await ajax.jsonRpc('/3Dstock/data/standalone', 'call', {
company_id: this.companyId,
loc_id: this.locationId,
});
this.locationData = locationData;
}

_createColorLegend() {
this.colorDiv = document.createElement("div");
this.colorDiv.classList.add("rectangle");
// Color squares with labels
const labels = [
{ color: 'square1', text: 'Overload' },
{ color: 'square2', text: 'Almost Full' },
{ color: 'square3', text: 'Free Space Available' },
{ color: 'square4blue', text: 'No Product/Load' },
];
labels.forEach(label => {
const square = document.createElement("div");
square.classList.add(label.color);
const text = document.createElement("div");
text.classList.add(`squareText${label.color.slice(-1)}`);
text.innerHTML = label.text;
this.colorDiv.appendChild(square);
this.colorDiv.appendChild(text);
});
}

_createBaseCube() {
const baseGeometry = new THREE.BoxGeometry(800, 0, 800);
const baseMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, opacity: 1 });
const baseCube = new THREE.Mesh(baseGeometry, baseMaterial);
this.scene.add(baseCube);
}

async _createLocationObjects() {
for (const [key, value] of Object.entries(this.locationData)) {
const [x, y, z, width, height, depth, locId] = value;

if (width > 0 || height > 0 || depth > 0) {
const geometry = new THREE.BoxGeometry(width, height, depth);
geometry.translate(0, height / 2, 0);

const edges = new THREE.EdgesGeometry(geometry);
const locationData = await ajax.jsonRpc('/3Dstock/data/quantity', 'call', { loc_code: key });

const locColor = this._determineLocationColor(locationData, locId);
const material = new THREE.MeshBasicMaterial({ color: locColor, transparent: true, opacity: 0.8 });

const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
mesh.userData = { locId, color: locColor };
this.scene.add(new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x404040 })));

this._addLocationText(mesh, key, width, height, depth);
this.group.add(mesh);
}
}
this.scene.add(this.group);
}

_determineLocationColor(locationData, locId) {
let locColor;
const quantity = locationData[1];

if (this.locationId === locId) {
if (quantity > 100) {
locColor = 0xcc0000;
} else if (quantity > 50) {
locColor = 0xe6b800;
} else {
locColor = 0x00802b;
}
} else {
locColor = 0x8c8c8c;
}
return locColor;
}

_addLocationText(mesh, key, width, height, depth) {
const loader = new THREE.FontLoader();
loader.load('https://threejs.org/examples/fonts/droid/droid_sans_bold.typeface.json', (font) => {
const textSize = Math.min(width, depth) / 3;
const textShapes = font.generateShapes(key, textSize);
const textGeometry = new THREE.ShapeGeometry(textShapes);
const textMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
textGeometry.translate(0, height / 2, 0);
textMesh.position.set(mesh.position.x, mesh.position.y, mesh.position.z);
this.scene.add(textMesh);
});
}

_onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight / 1.163);
}

_onPointerMove(event) {
const canvasBounds = this.renderer.domElement.getBoundingClientRect();
this.pointer.x = ((event.clientX - canvasBounds.left) / canvasBounds.width) * 2 - 1;
this.pointer.y = -((event.clientY - canvasBounds.top) / canvasBounds.height) * 2 + 1;

this.raycaster.setFromCamera(this.pointer, this.camera);
const intersects = this.raycaster.intersectObjects(this.group.children);

if (intersects.length > 0) {
const selectedObject = intersects[0].object;
if (selectedObject.userData.locId) {
this.dialog = new PositionDialog(this, {
pointer: { x: event.clientX, y: event.clientY },
close: () => this.dialog.close()
});
this.dialog.mount(this.el);
}
}
}

_animate() {
requestAnimationFrame(this._animate.bind(this));
this.renderer.render(this.scene, this.camera);
}
}

registry.category('actions').add('open_form_3d_view', OpenForm3DView);


/** @odoo-module **/
import { ListController } from '@web/views/list/list_controller';
import { registry } from '@web/core/registry';
import { ajax, rpc } from '@web/core/network/rpc_service';
import { Dialog } from '@web/core/dialog/dialog';
import { useService } from '@web/core/utils/hooks';
import { QWeb } from '@web/core/qweb';
import { session } from '@web/session';

const { Component } = owl;
const THREE = require('three');
require('three/examples/js/controls/OrbitControls');

class PositionDialog extends Dialog {
setup() {
super.setup();
this.pointer = this.props.pointer;
this.onClickClose = this.props.close;
}

mounted() {
this.el.querySelector('.modal-dialog').style.position = 'absolute';
this.el.querySelector('.modal-dialog').style.left = `${this.pointer.x}px`;
this.el.querySelector('.modal-dialog').style.top = `${this.pointer.y}px`;
}
}

class TreeButton extends ListController {
setup() {
super.setup();
this.dialogService = useService('dialog');
}

async _Open3DView(ev) {
let wh_data, data, loc_quant, controls, renderer, clock, scene, camera, pointer, raycaster;
let group, loc_color, loc_opacity = 0.5;
let wh_id;
let selectedObject = null;

await ajax.jsonRpc('/3Dstock/warehouse', 'call', {
'company_id': session.user_context.allowed_company_ids[0],
}).then(incoming_data => {
wh_data = incoming_data;
});

wh_id = wh_data[0][0];
const select = document.createElement("select");
select.classList.add("customselect");

wh_data.forEach(([id, name]) => {
const option = document.createElement("option");
option.value = id;
option.text = name;
select.appendChild(option);
});

const closeDiv = document.createElement("button");
closeDiv.classList.add("closeBtn");
closeDiv.innerHTML = "&times;";
const colorDiv = this._createColorLegend();

this._start3DView({ select, closeDiv, colorDiv, wh_id });
}

_createColorLegend() {
const colorDiv = document.createElement("div");
colorDiv.classList.add("rectangle");

const colors = [
{ class: 'square1', text: 'Overload' },
{ class: 'square2', text: 'Almost Full' },
{ class: 'square3', text: 'Free Space Available' },
{ class: 'square4', text: 'No Product/Load' },
];

colors.forEach(({ class: className, text }) => {
const square = document.createElement("div");
square.classList.add(className);
colorDiv.appendChild(square);

const textDiv = document.createElement("div");
textDiv.classList.add(`${className}Text`);
textDiv.innerHTML = text;
colorDiv.appendChild(textDiv);
});

return colorDiv;
}

async _start3DView({ select, closeDiv, colorDiv, wh_id }) {
let data, loc_quant, controls, renderer, clock, scene, camera, group, loc_color, loc_opacity = 0.5;

await ajax.jsonRpc('/3Dstock/data', 'call', {
'company_id': session.user_context.allowed_company_ids[0],
'wh_id': wh_id,
}).then(incoming_data => {
data = incoming_data;
});

scene = new THREE.Scene();
scene.background = new THREE.Color(0xdfdfdf);
clock = new THREE.Clock();
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.5, 6000);
camera.position.set(0, 200, 300);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight / 1.164);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.render(scene, camera);

this.el.querySelector('.o_content').append(renderer.domElement, select, colorDiv, closeDiv);

const dropdown = document.querySelector(".customselect");
dropdown.addEventListener("change", this._warehouseChange.bind(this));

closeDiv.addEventListener("click", () => window.location.reload());
controls = new THREE.OrbitControls(camera, renderer.domElement);

const baseGeometry = new THREE.BoxGeometry(800, 0, 800);
const baseMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.BackSide });
const baseCube = new THREE.Mesh(baseGeometry, baseMaterial);
scene.add(baseCube);

group = new THREE.Group();
for (const [key, value] of Object.entries(data)) {
if ((value[0] > 0) || (value[1] > 0) || (value[2] > 0) || (value[3] > 0) || (value[4] > 0) || (value[5] > 0)) {
const geometry = new THREE.BoxGeometry(value[3], value[5], value[4]);
geometry.translate(0, (value[5] / 2), 0);
const edges = new THREE.EdgesGeometry(geometry);

await ajax.jsonRpc('/3Dstock/data/quantity', 'call', { 'loc_code': key }).then(quant_data => {
loc_quant = quant_data;
});

loc_color = this._getLocationColor(loc_quant);
loc_opacity = loc_quant[1] > 0 ? 0.8 : 0.5;

const material = new THREE.MeshBasicMaterial({
color: loc_color,
transparent: true,
opacity: loc_opacity
});
const mesh = new THREE.Mesh(geometry, material);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x404040 }));

line.position.set(value[0], value[1], value[2]);
mesh.position.set(value[0], value[1], value[2]);

this._addTextLabel({ key, value, textSize: Math.min(value[3], value[4]) / 2, scene });
scene.add(mesh, line);
group.add(mesh);
}
}

scene.add(group);
this._animate({ renderer, scene, camera, clock });
}

_getLocationColor(loc_quant) {
if (loc_quant[0] > 0) {
if (loc_quant[1] > 100) return 0xcc0000;
else if (loc_quant[1] > 50) return 0xe6b800;
else return 0x00802b;
}
return loc_quant[1] === -1 ? 0x00802b : 0x8c8c8c;
}

_addTextLabel({ key, value, textSize, scene }) {
const loader = new THREE.FontLoader();
loader.load('https://threejs.org/examples/fonts/droid/droid_sans_bold.typeface.json', function(font) {
const textMat = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide });
const textshapes = font.generateShapes(key, textSize);
const textgeometry = new THREE.ShapeGeometry(textshapes);
textgeometry.translate(0, (value[5] / 2) - (textSize / (textSize - 1.5)), 0);
const text = new THREE.Mesh(textgeometry, textMat);

if (value[4] > value[3]) {
text.rotation.y = Math.PI / 2;
text.position.set(value[0], value[1], value[2] + (textSize * 2));
} else {
text.position.set(value[0] - (textSize * 2), value[1], value[2]);
}
scene.add(text);
});
}

_animate({ renderer, scene, camera, clock }) {
const animate = () => {
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
}

_warehouseChange() {
const selectedBox = document.querySelector(".customselect");
const selectValue = selectedBox.value;
this._start3DView({ wh_id: selectValue });
}
}

registry.category('views').add('button_near_create.tree_button', {
Controller: TreeButton,
});





0
อวตาร
ละทิ้ง
อวตาร
Cybrosys Techno Solutions Pvt.Ltd
คำตอบที่ดีที่สุด

Hi,


To update your Odoo 17 component with the new OWL patterns:


1) Add new imports for Odoo 17 features like useState, useService and remove deprecated imports like web.ajax :


// Old imports

import { Dialog } from '@web/core/dialog/dialog';

import { Component } from '@odoo/owl';

import ajax from 'web.ajax';


// New imports

import { Dialog } from "@web/core/dialog/dialog";

import { Component, useState } from "@odoo/owl";

import { useService } from "@web/core/utils/hooks";


2) Update component structure to include static properties for template and components, in line with the new OWL standard:


// Old

class OpenForm3DView extends Component {

setup() {

super.setup();

// ...

}

}


// New

export class OpenForm3DView extends Component {

static template = "stock_3d_view.OpenForm3DView";

static components = { Dialog };


setup() {

super.setup();

// ...

}

}


3) Replace access to services via this.env.services with useService:


// Old

setup() {

    this.actionService = this.env.services.action;

    // ...

}


// New

setup() {

    this.actionService = useService("action");

    this.orm = useService("orm");

    // ...

}


4) Update data fetching to use orm.call instead of ajax.jsonRpc.


// Old

async _fetchData() {

    const locationData = await ajax.jsonRpc('/3Dstock/data/standalone', 'call', {

        company_id: this.companyId,

        loc_id: this.locationId,

    });

    this.locationData = locationData;

}


// New

async _fetchData() {

    this.locationData = await this.orm.call(

        "stock.location",

        "get_3d_stock_data",

        [this.companyId, this.locationId]

    );

}


Hope it helps

0
อวตาร
ละทิ้ง
สนุกกับการพูดคุยนี้ใช่ไหม? เข้าร่วมเลย!

สร้างบัญชีวันนี้เพื่อเพลิดเพลินไปกับฟีเจอร์พิเศษและมีส่วนร่วมกับคอมมูนิตี้ที่ยอดเยี่ยมของเรา!

ลงชื่อ
Related Posts ตอบกลับ มุมมอง กิจกรรม
Error while posting invoice to ZATCA odoo sh แก้ไขแล้ว
odoo17
อวตาร
อวตาร
อวตาร
อวตาร
3
ก.ค. 25
3321
How to send a real-time notification to POS UI using bus.bus in Odoo 17?
odoo17
อวตาร
อวตาร
1
มิ.ย. 25
5785
Odoo time-sheets rights to add
odoo17
อวตาร
อวตาร
2
พ.ค. 25
3220
POS is not recognizing short barcodes like "95" or "96" (Code 39 or custom short codes)
odoo17
อวตาร
1
พ.ค. 25
2212
Odoo 17
odoo17
อวตาร
1
ก.พ. 25
40
คอมมูนิตี้
  • บทเรียน
  • เอกสารกำกับโปรแกรม
  • ฟอรั่ม
โอเพนซอร์ส
  • ดาวน์โหลด
  • Github
  • Runbot
  • การแปล
บริการ
  • โฮสติ้ง odoo.sh เซิร์ฟเวอร์
  • การสนับสนุน
  • อัปเกรด
  • การพัฒนาที่กำหนดเอง
  • การศึกษา
  • ค้นหานักบัญชี
  • ค้นหาพาร์ทเนอร์
  • ร่วมเป็นพาร์ทเนอร์
เกี่ยวกับเรา
  • บริษัทของเรา
  • ทรัพย์สินในรูปของเครื่องหมายการค้า
  • ติดต่อเรา
  • งาน
  • ระบบการจัดงานอีเวนต์
  • พอดแคสต์
  • บล็อก
  • ลูกค้า
  • กฎหมาย • ความเป็นส่วนตัว
  • ความปลอดภัย
الْعَرَبيّة Català 简体中文 繁體中文 (台灣) Čeština Dansk Nederlands English Suomi Français Deutsch हिंदी Bahasa Indonesia Italiano 日本語 한국어 (KR) Lietuvių kalba Język polski Português (BR) română русский язык Slovenský jazyk slovenščina Español (América Latina) Español ภาษาไทย Türkçe українська Tiếng Việt

Odoo เป็นชุดแอปธุรกิจแบบเปิดที่ครอบคลุมทุกความต้องการของบริษัทของคุณ อาธิ CRM eCommerce ระบบบัญชี ระบบสินค้าคงคลัง ระบบการขายหน้าร้าน โปรเจกต์ และ อีกมากมาย

ความเหนือกว่าอันเป็นเอกลักษณ์ของ Odoo คือการใช้งานที่ง่ายดายและผสานการทำงานอย่างสมบูรณ์แบบในเวลาเดียวกัน

Website made with

Odoo Experience on YouTube

1. Use the live chat to ask your questions.
2. The operator answers within a few minutes.

Live support on Youtube
Watch now