Created
February 10, 2025 21:23
-
-
Save hsayed21/397dd3442ccce86216d64b6021ec28b4 to your computer and use it in GitHub Desktop.
Odoo Auto Fill Ticket
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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