Skip to content

Instantly share code, notes, and snippets.

@hsayed21
Created February 10, 2025 21:23
Show Gist options
  • Save hsayed21/397dd3442ccce86216d64b6021ec28b4 to your computer and use it in GitHub Desktop.
Save hsayed21/397dd3442ccce86216d64b6021ec28b4 to your computer and use it in GitHub Desktop.
Odoo Auto Fill Ticket
// ==UserScript==
// @name Odoo Auto Fill Ticket
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Auto fill Odoo ticket forms after page load
// @author @hsayed21
// @match https://www.posbank.me/web
// @icon https://www.google.com/s2/favicons?sz=64&domain=posbank.me
// @updateURL https://gist.github.com/hsayed21/397dd3442ccce86216d64b6021ec28b4.js
// @downloadURL https://gist.github.com/hsayed21/397dd3442ccce86216d64b6021ec28b4.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Configuration object containing all settings
const CONFIG = {
timeouts: {
element: 30000, // Maximum time to wait for an element
dropdown: 1000, // Time to wait for dropdown to appear
check: 100, // Interval for checking dropdown visibility
delay: 2000 // General delay between operations
},
formFields: {
'ticket_problem_id': 'issue',
'team_id': 'Customer Care',
'user_id': 'Support Team',
'responsible_id': 'Hamada Sayed Hamed',
'ticket_type_id': 'Issue - Customer'
},
validation: {
phoneFormat: /^\+\d{3}\s\d{4}\s\d{4}$/, // Format: +968 9392 4460
urlPattern: {
baseUrl: 'posbank.me',
requiredParams: {
view_type: 'form',
model: 'helpdesk.ticket',
action: '1280'
},
checkInterval: 1000
}
}
};
// DOM Helper functions
const DOMHelper = {
async waitForElement(selector) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
} else if (Date.now() - startTime > CONFIG.timeouts.element) {
observer.disconnect();
reject(new Error(`Element ${selector} not found`));
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
},
async waitForVisibleDropdown() {
return new Promise((resolve) => {
let attempts = 0;
const maxAttempts = CONFIG.timeouts.dropdown / CONFIG.timeouts.check;
const checkDropdown = () => {
const dropdowns = document.querySelectorAll('ul[id^="ui-id-"]');
const visibleDropdown = Array.from(dropdowns)
.find(dropdown => window.getComputedStyle(dropdown).display === 'block');
if (visibleDropdown) {
resolve(visibleDropdown);
return;
}
if (++attempts < maxAttempts) {
setTimeout(checkDropdown, CONFIG.timeouts.check);
} else {
resolve(null);
}
};
checkDropdown();
});
}
};
// Form handling functions
const FormHandler = {
async setDropdownField(element, value) {
if (!element) return false;
try {
// Focus and clear the field
element.focus();
element.value = '';
element.dispatchEvent(new Event('input', { bubbles: true }));
// Set new value
element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
// Handle dropdown selection
const dropdown = await DOMHelper.waitForVisibleDropdown();
if (dropdown) {
const firstOption = dropdown.querySelector('li');
if (firstOption) {
firstOption.click();
}
}
// Finalize the field
element.value = value;
element.dispatchEvent(new Event('change', { bubbles: true }));
element.blur();
return true;
} catch (error) {
console.error(`Error setting field: ${value}`, error);
return false;
}
},
async fillForm() {
try {
await DOMHelper.waitForElement('.o_form_sheet');
const phoneNumber = await ClipboardHandler.getValidPhoneNumber();
// Fill standard form fields
for (const [fieldName, value] of Object.entries(CONFIG.formFields)) {
const element = await DOMHelper.waitForElement(`div[name='${fieldName}'] input`);
await this.setDropdownField(element, value);
}
// Handle phone number specific fields if available
if (phoneNumber) {
await this.handlePhoneNumberFields(phoneNumber);
}
} catch (error) {
console.error('Form fill error:', error);
}
},
async handlePhoneNumberFields(phoneNumber) {
// Set partner_id
const partnerElement = await DOMHelper.waitForElement(`div[name='partner_id'] input`);
await this.setDropdownField(partnerElement, phoneNumber);
// Set distributor_id if empty
await new Promise(resolve => setTimeout(resolve, CONFIG.timeouts.delay));
const distributorElement = await DOMHelper.waitForElement(`div[name='distributor_id'] input`);
if (distributorElement.value === '') {
await this.setDropdownField(distributorElement, phoneNumber);
}
}
};
// Clipboard handling functions
const ClipboardHandler = {
async readFromClipboard() {
const elements = this.createClipboardElements();
try {
return await this.attemptClipboardRead(elements);
} finally {
this.cleanupElements(elements);
}
},
createClipboardElements() {
const textarea = document.createElement('textarea');
const contentEditable = document.createElement('div');
textarea.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
contentEditable.contentEditable = true;
contentEditable.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
document.body.appendChild(textarea);
document.body.appendChild(contentEditable);
return { textarea, contentEditable };
},
async attemptClipboardRead({ textarea, contentEditable }) {
// Try modern Clipboard API
if (navigator.clipboard?.readText) {
try {
const text = await navigator.clipboard.readText();
if (text) return text;
} catch (e) {
console.log('Clipboard API failed, trying fallback methods...');
}
}
// Try execCommand on textarea
textarea.focus();
if (document.execCommand('paste')) {
const text = textarea.value;
if (text) return text;
}
// Try execCommand on contentEditable
contentEditable.focus();
if (document.execCommand('paste')) {
return contentEditable.innerText;
}
return null;
},
cleanupElements({ textarea, contentEditable }) {
document.body.removeChild(textarea);
document.body.removeChild(contentEditable);
},
async getValidPhoneNumber() {
const clipboardContent = await this.getClipboardContent();
return clipboardContent && CONFIG.validation.phoneFormat.test(clipboardContent)
? clipboardContent.trim()
: null;
},
async getClipboardContent() {
const maxAttempts = 3;
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
for (let attempts = 0; attempts < maxAttempts; attempts++) {
try {
const text = await this.readFromClipboard();
if (text) return text.trim();
await delay(500);
} catch (error) {
console.log(`Clipboard read attempt ${attempts + 1} failed:`, error);
await delay(500);
}
}
console.error('Failed to read clipboard after multiple attempts');
return null;
}
};
// URL handling and monitoring
class URLMonitor {
constructor(callback) {
this.lastUrl = location.href;
this.callback = callback;
this.isRunning = false;
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.startIntervalCheck();
this.startMutationObserver();
}
stop() {
if (!this.isRunning) return;
clearInterval(this.intervalId);
this.observer.disconnect();
this.isRunning = false;
}
startIntervalCheck() {
this.intervalId = setInterval(() => {
this.checkUrlChange();
}, CONFIG.validation.urlPattern.checkInterval);
}
startMutationObserver() {
this.observer = new MutationObserver(() => this.checkUrlChange());
this.observer.observe(document, {
subtree: true,
childList: true
});
}
checkUrlChange() {
const currentUrl = location.href;
if (currentUrl !== this.lastUrl) {
console.log('URL changed:', {
from: this.lastUrl,
to: currentUrl
});
this.lastUrl = currentUrl;
this.callback();
}
}
}
// URL validation
const URLValidator = {
isTargetPage() {
const url = new URL(window.location.href);
if (!url.origin.includes(CONFIG.validation.urlPattern.baseUrl)) return false;
const hashPart = url.hash.substring(1);
if (!hashPart) return false;
try {
const params = Object.fromEntries(new URLSearchParams(hashPart));
const hasAllRequiredParams = Object.entries(CONFIG.validation.urlPattern.requiredParams)
.every(([key, value]) => params[key] === value);
const idIsValid = !params.id || !/^\d{4}$/.test(params.id);
return hasAllRequiredParams && idIsValid;
} catch (error) {
console.error('Error parsing URL:', error);
return false;
}
}
};
// Main initialization
function init() {
const urlMonitor = new URLMonitor(() => {
if (URLValidator.isTargetPage()) {
console.log('Target page detected, starting form fill');
FormHandler.fillForm();
} else {
console.log('Not on target page');
}
});
urlMonitor.start();
if (URLValidator.isTargetPage()) {
console.log('Initial target page detected, starting form fill');
FormHandler.fillForm();
}
}
// Start the script
init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment