// ==UserScript==
// @name IDEA KFZ Listing
// @namespace http://tampermonkey.net/
// @version 3.1
// @description Ulepszenia dla listy faktur KFZ w systemie IDEA ERP
// @author Adam
// @match https://*.ideaerp.pl/*
// @icon https://www.ideaerp.pl/favicon.ico
// @downloadURL https://n8n.emma.net.pl/webhook/kfz-listing
// @updateURL https://n8n.emma.net.pl/webhook/kfz-listing
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// ==/UserScript==
/* ========================OPIS==============================
WERSJA 3.1 - Uproszczona logika wykrywania strony
DZIAŁANIE SKRYPTU:
- Wykrywa strony z listą faktur KFZ w systemie IDEA ERP
- Dodaje ulepszenia wizualne tabeli (sticky header, podświetlanie)
- Implementuje szybkie filtry według dostawców
- Umożliwia kopiowanie zawartości komórek w kolumnach:
* Numer faktury (data-name="name")
* Data faktury (data-name="invoice_date")
* Odnośnik (data-name="invoice_origin")
* Należność (data-name="amount_residual_signed" - NIE NETTO!)
* Na podstawie (data-name="reversed_entry_id")
- Pokazuje powiadomienia o skopiowaniu
ZMIANY W WERSJI 3.1:
- 🔍 UPROSZCZONA logika wykrywania strony - bardziej liberalne warunki
- ✅ Działaj jeśli: action=278+menu_id=141 LUB breadcrumb="Korekty" LUB numery KFZ
- 📊 Dodano szczegółowe logi w initEnhancements()
- 🚀 Usunięto zbędne skomplikowane warunki
- 🎯 Focus na działanie, nie na perfekcyjne wykrywanie
ZMIANY W WERSJI 3.0:
- 🔄 POWRÓT do stabilnej wersji bez waitForPageLoad()
- ❌ USUNIĘTO skomplikowaną funkcję waitForPageLoad() która powodowała błędy
- ✅ PRZYWRÓCONO prostą logikę z setTimeout(initEnhancements)
- 🎯 Wymagane action=278 + menu_id=141 dla aktywacji
- 📊 Zachowano podstawowe logi debugowania
- 🚀 Punkt wyjścia do dalszych ulepszeń
ZMIANY W WERSJI 2.9.1:
- 🐛 NAPRAWIONO błąd "hasAction278 is not defined"
- 🔧 Poprawiono wszystkie odwołania do zmiennych w waitForPageLoad()
- ✅ Usunięto błędy JavaScript które blokowały działanie skryptu
ZMIANY W WERSJI 2.9:
- 🔍 DODANO szczegółowe debugowanie w waitForPageLoad() i isKfzListingPage()
- 📊 Emoji w logach dla lepszej czytelności
- 🎯 Wymagane action=278 + menu_id=141 dla aktywacji
- 📍 Logi URL i elementów na starcie skryptu
- 🚀 Test czy skrypt w ogóle się ładuje
- 🔧 Diagnostyka dla problemu "nie działa w ogóle"
ZMIANY W WERSJI 2.8:
- 🔄 DODANO resetowanie stanu przy zmianie URL (hashchange i polling)
- 🔍 Sprawdzanie czy elementy rzeczywiście istnieją mimo markera
- 🧹 Automatyczne usuwanie markerów przy zmianie strony
- 🎯 Dodano hasKfzNumbers jako dodatkowy warunek aktywacji
- 📊 Szczegółowe logi resetowania stanu
- ✅ Rozwiązano problem niespójnego działania przy nawigacji
ZMIANY W WERSJI 2.7:
- 🕐 DODANO funkcję waitForPageLoad() - inteligentne oczekiwanie na elementy
- 🔄 Maksymalnie 10 prób co 1 sekundę na załadowanie tabeli/breadcrumb
- 🚫 Wykrywanie elementów ładowania (.o_loading, .fa-spin) - czeka aż znikną
- 🎯 Wszystkie wywołania initEnhancements zastąpione waitForPageLoad
- 📊 Szczegółowe logi procesu oczekiwania na załadowanie
- ✅ Rozwiązano problem "Table: false, Breadcrumb: null" po odświeżeniu
ZMIANY W WERSJI 2.6:
- 🎯 DODANO wykrywanie strony przez breadcrumb "Korekty"
- 🔄 ZMIENIONA logika: aktywacja nawet bez danych KFZ (mogą się załadować później)
- 📍 Backup timer sprawdza breadcrumb co 3 sekundy
- 🚀 Rozwiązano problem z URL: action=278&model=account.move&view_type=list&cids=1&menu_id=141
- 📊 Dodatkowe logi dla debugowania wykrywania strony
ZMIANY W WERSJI 2.5:
- 🔄 DODANO wielopoziomowe wykrywanie zmian URL dla aplikacji SPA
- 📍 5 metod wykrywania: hashchange, URL polling, title observer, breadcrumb observer, backup timer
- ⚡ Szybsze reakcje: 200ms, 300ms, 400ms, 500ms, 1000ms, 1500ms
- 🎯 Rozwiązano problem z brakiem działania przy nawigacji (bez odświeżania)
- 🔍 Backup sprawdzanie co 3 sekundy czy strona KFZ ma filtr
- 📊 Szczegółowe logi dla debugowania zmian URL
ZMIANY W WERSJI 2.4:
- 🔧 PRZENIESIONO filtr za przyciski w lewym panelu (.o_cp_left)
- 🚫 ROZWIĄZANO problem nakładania się na wyszukiwarkę
- 📍 Filtr teraz w linii: [Zapisz] [Odrzuć] [🔽 Dostawca] | [Wyszukiwarka]
- 💫 Lepsze wykorzystanie przestrzeni bez konfliktów
ZMIANY W WERSJI 2.3:
- 🔧 NAPRAWIONO pozycjonowanie filtra - teraz jest OBOK wyszukiwarki
- 📍 Filtr wstawiony do `.o_searchview_input_container` zamiast `.o_cp_searchview`
- 💫 Filtr i wyszukiwarka w tej samej linii (flex-shrink: 0)
- 🎯 Lepsze wykorzystanie przestrzeni w panelu kontrolnym
ZMIANY W WERSJI 2.2:
- 🎨 PRZENIESIONO filtr dostawców do panelu kontrolnego (.o_cp_searchview)
- 💫 Filtr ma teraz profesjonalny wygląd zgodny z interfejsem Odoo
- 📍 Umieszczony obok wyszukiwarki z ikoną i odpowiednim stylem
- 🔄 Dodano fallback - jeśli brak panelu, filtr pojawi się przed tabelą
- 🎯 Zaktualizowano funkcję filtrowania na nowe indeksy pozycyjne
ZMIANY W WERSJI 2.1:
- 🔧 NAPRAWIONO błąd TypeError w isKfzListingPage()
- 🎯 ZASTĄPIONO wyszukiwanie po data-name na indeksy pozycyjne
- 📍 Mapowanie kolumn na podstawie rzeczywistej struktury:
* Komórka 2: Numer faktury (KFZ/...)
* Komórka 4: Data faktury (DD.MM.YYYY)
* Komórka 5: Dokument źródłowy (ZZ/...)
* Komórka 6: Odnośnik (KFAS/...)
* Komórka 10: Należność (...zł)
* Komórka 11: Na podstawie (KFZ/... (...))
- ✅ Wszystkie kolumny powinny teraz działać poprawnie!
ZMIANY W WERSJI 2.0:
- Dodano bardzo szczegółowe debugowanie dla wszystkich kolumn
- Logowanie struktury pierwszego wiersza z klasami CSS
- Szczegółowe logi dla każdej poszukiwanej kolumny:
* invoice_origin (Dokument źródłowy)
* ref (Odnośnik)
* amount_residual_signed (Należność)
* reversed_entry_id (Na podstawie)
- Informowanie o pustych wartościach i myślnikach
- Wyraźne oznaczenie ❌ gdy kolumna nie została znaleziona
ZMIANY W WERSJI 1.9:
- Usunięto niepotrzebne kopiowanie z kolumny NETTO (amount_untaxed_signed)
- Dodano sprawdzanie data-name żeby unikać kopiowania z NETTO
- Pozostawiono tylko kopiowanie z kolumny NALEŻNOŚĆ (amount_residual_signed)
- Ograniczono debugowanie do pierwszego wiersza
- Dodano szczegółowe logowanie dla każdej kolumny
ZMIANY W WERSJI 1.8:
- Dodano szczegółowe debugowanie struktury tabeli
- Fallback dla różnych formatów data-name (małe/duże litery)
- Alternatywne wyszukiwanie po zawartości komórek
- Debugowanie wszystkich komórek w wierszach
- Logowanie procesu dodawania kopiowania
- Sprawdzanie czy w ogóle są wiersze danych
ZMIANY W WERSJI 1.7:
- Zmieniono z indeksów kolumn na querySelector z data-name
- Numer: td[data-name="name"]
- Data faktury: td[data-name="invoice_date"]
- Odnośnik: td[data-name="invoice_origin"]
- Należność: td[data-name="amount_residual_signed"]
- Na podstawie: td[data-name="reversed_entry_id"]
- Dodano szczegółowe logowanie dla każdej kolumny
- Rozwiązano problem z ukrytymi kolumnami
ZMIANY W WERSJI 1.6:
- Zmieniono nazwę skryptu na "IDEA KFZ Listing"
- Poprawiono mapowanie kolumny "Należność" (amount_residual_signed, indeks 9)
- Naprawiono kopiowanie kolumny "Odnośnik" (invoice_origin, indeks 5)
- Dodano debugowanie dla kolumny "Odnośnik"
- Wykluczenie pustych wartości i myślników "-" z kopiowania
ZMIANY W WERSJI 1.5:
- Rozszerzone kopiowanie na 5 kolumn (Numer, Data faktury, Odnośnik, Należność, Na podstawie)
- Ulepszone czyszczenie kwot (usuwanie "zł" i znaków niełamliwych spacji)
- Automatyczne wykrywanie kolumny "Na podstawie" przez data-name="reversed_entry_id"
- Inteligentne wykrywanie dat w formacie DD.MM.YYYY
- Kopiowanie odnośników (dokumentów źródłowych)
ZMIANY W WERSJI 1.4:
- Dodano zabezpieczenie przed duplikowaniem elementów
- Sprawdzanie czy skrypt już został uruchomiony (.kfz-script-initialized)
- Unikalne ID dla stylów CSS (#kfz-copy-styles, #kfz-table-styles)
- Sprawdzanie istnienia filtrów (.kfz-filter-container)
- Eliminacja powielania elementów przy wielokrotnym uruchomieniu
ZMIANY W WERSJI 1.3:
- Obsługa dynamicznego ładowania danych przez AJAX
- Przechwytywanie żądań do /web/dataset/search_read
- Wykrywanie różnicy między widokiem grupowanym a szczegółowym
- Uruchamianie skryptu po kliknięciu na dostawcę
- Inteligentny MutationObserver dla nowych wierszy danych
ZMIANY W WERSJI 1.2:
- Poprawione wykrywanie strony KFZ (sprawdza URL account.move)
- Dodano obserwację zmian DOM dla dynamicznych stron
- Lepsze debugowanie z logami w konsoli
- Obsługa zmian URL (hashchange)
- MutationObserver dla automatycznego uruchamiania
ZMIANY W WERSJI 1.1:
- Dodano funkcjonalność kopiowania w stylu idea-orders.user.js
- Ikony kopiowania ⧉ po prawej stronie numerów KFZ (pomarańczowe)
- Kliknięcie na komórkę z numerem kopiuje do schowka
- Efekt wizualny po skopiowaniu (pomarańczowa ramka)
- Fallback dla starszych przeglądarek
- Dodano @updateURL i @downloadURL zgodnie z zasadami
WERSJA 1.0 - Podstawowe ulepszenia listy KFZ
- Wykrywanie strony z listą faktur KFZ
- Ulepszenia wizualne tabeli
- Szybkie filtry według dostawców
- System logowania z flagą DEBUG
============================================================ */
(() => {
'use strict';
// [Tampermonkey] Konfiguracja
const DEBUG = true;
const SCRIPT_NAME = '[IDEA KFZ Listing]';
/**
* Funkcja logowania z prefiksem
* @param {string} message - Wiadomość do zalogowania
*/
function log(message) {
if (DEBUG) {
console.log(`${SCRIPT_NAME} ${message}`);
}
}
/**
* Sprawdza czy jesteśmy na stronie z listą faktur KFZ
* @returns {boolean}
*/
function isKfzListingPage() {
// Sprawdź URL - czy to model account.move
const url = window.location.href;
const isAccountMoveList = url.includes('model=account.move') && url.includes('view_type=list');
const hasAction278 = url.includes('action=278');
const hasMenu141 = url.includes('menu_id=141');
// Sprawdź czy jest tabela
const table = document.querySelector('.o_list_table');
// Sprawdź czy są numery KFZ w różnych formatach
const hasKfzNumbers = document.querySelector('td[title*="KFZ/"]') ||
Array.from(document.querySelectorAll('td')).some(td => td.textContent.includes('KFZ/')) ||
(document.textContent && document.textContent.includes('KFZ/'));
// Sprawdź czy są wiersze danych (nie tylko grupowanie)
const hasDataRows = document.querySelectorAll('.o_data_row').length > 0;
// Sprawdź czy to nie jest widok grupowany (ma grupy ale nie ma szczegółów)
const hasGroups = document.querySelectorAll('.o_group_header').length > 0;
const isGroupedView = hasGroups && !hasDataRows;
// Sprawdź breadcrumb - czy zawiera "Korekty"
const breadcrumb = document.querySelector('.breadcrumb');
const hasCorrectionsBreadcrumb = breadcrumb && breadcrumb.textContent.includes('Korekty');
log(`URL: ${isAccountMoveList}, Action278: ${hasAction278}, Menu141: ${hasMenu141}, Table: ${!!table}, KFZ: ${!!hasKfzNumbers}, DataRows: ${hasDataRows}, Breadcrumb: ${hasCorrectionsBreadcrumb}`);
// Bardziej liberalne warunki - działaj jeśli:
// 1. Jest action=278 + menu_id=141 (specificzny URL dla KFZ)
// 2. LUB breadcrumb zawiera "Korekty"
// 3. LUB znaleziono numery KFZ w treści strony
const shouldActivate = (hasAction278 && hasMenu141) ||
hasCorrectionsBreadcrumb ||
(isAccountMoveList && hasKfzNumbers);
log(`Czy aktywować: ${shouldActivate}`);
return shouldActivate;
}
/**
* Czyści duplikaty elementów z poprzednich uruchomień
*/
function cleanupDuplicates() {
// Usuń duplikaty filtrów
const filterContainers = document.querySelectorAll('.kfz-filter-container');
if (filterContainers.length > 1) {
for (let i = 1; i < filterContainers.length; i++) {
filterContainers[i].remove();
}
log(`Usunięto ${filterContainers.length - 1} duplikatów filtrów`);
}
// Usuń duplikaty stylów
const copyStyles = document.querySelectorAll('#kfz-copy-styles');
if (copyStyles.length > 1) {
for (let i = 1; i < copyStyles.length; i++) {
copyStyles[i].remove();
}
log(`Usunięto ${copyStyles.length - 1} duplikatów stylów kopiowania`);
}
const tableStyles = document.querySelectorAll('#kfz-table-styles');
if (tableStyles.length > 1) {
for (let i = 1; i < tableStyles.length; i++) {
tableStyles[i].remove();
}
log(`Usunięto ${tableStyles.length - 1} duplikatów stylów tabeli`);
}
}
/**
* Główna funkcja inicjalizująca ulepszenia
*/
async function initEnhancements() {
try {
log(`🔍 INIT ENHANCEMENTS - sprawdzam stronę: ${window.location.href}`);
if (!isKfzListingPage()) {
log('❌ Nie wykryto strony z listą KFZ');
return;
}
log('✅ WYKRYTO STRONĘ KFZ - kontynuuję...');
// Sprawdź czy skrypt już został uruchomiony na tej stronie
const scriptMarker = document.querySelector('.kfz-script-initialized');
if (scriptMarker) {
log('Skrypt już został uruchomiony - sprawdzam czy elementy istnieją');
// Sprawdź czy elementy rzeczywiście istnieją
const hasFilter = document.querySelector('.kfz-filter-container');
const hasStyles = document.querySelector('#kfz-copy-styles');
if (!hasFilter || !hasStyles) {
log('Elementy skryptu nie istnieją mimo markera - resetuję i uruchamiam ponownie');
scriptMarker.remove();
// Usuń wszystkie elementy skryptu
document.querySelectorAll('.kfz-filter-container, #kfz-copy-styles, #kfz-table-styles').forEach(el => el.remove());
} else {
log('Elementy skryptu istnieją - pomijam');
return;
}
}
// Usuń ewentualne duplikaty z poprzednich uruchomień
cleanupDuplicates();
log('Wykryto stronę z listą faktur KFZ');
// Oznacz że skrypt został uruchomiony
const marker = document.createElement('div');
marker.className = 'kfz-script-initialized';
marker.style.display = 'none';
document.body.appendChild(marker);
// Tutaj można dodać konkretne ulepszenia
enhanceTableVisibility();
addQuickFilters();
addCopyStyles();
addCopyFunctionality();
log('Wszystkie ulepszenia zostały zastosowane');
} catch (error) {
console.error(`${SCRIPT_NAME} Błąd:`, error);
}
}
/**
* Poprawia widoczność tabeli
*/
function enhanceTableVisibility() {
const table = document.querySelector('.o_list_table');
if (!table) return;
// Sprawdź czy style już zostały dodane
if (document.querySelector('#kfz-table-styles')) {
log('Style tabeli już istnieją - pomijam dodawanie');
return;
}
// Dodaj style CSS dla lepszej czytelności
const style = document.createElement('style');
style.id = 'kfz-table-styles';
style.textContent = `
.o_list_table .text-danger {
background-color: #fff5f5 !important;
border-left: 3px solid #dc3545 !important;
}
.o_list_table .o_data_row:hover {
background-color: #f8f9fa !important;
}
.o_list_table th {
position: sticky;
top: 0;
background-color: #fff !important;
z-index: 10;
}
`;
document.head.appendChild(style);
log('Zastosowano ulepszenia wizualne tabeli');
}
/**
* Dodaje szybkie filtry do panelu kontrolnego
*/
function addQuickFilters() {
// Sprawdź czy filtry już istnieją
if (document.querySelector('.kfz-filter-container')) {
log('Filtry już istnieją - pomijam dodawanie');
return;
}
// Znajdź lewą część panelu kontrolnego (za przyciskami)
const leftPanel = document.querySelector('.o_cp_left .o_cp_buttons');
if (!leftPanel) {
log('Nie znaleziono lewego panelu - dodaję filtr przed tabelą');
addQuickFiltersBeforeTable();
return;
}
// Pobierz unikalnych dostawców z wierszy danych
const suppliers = new Set();
const rows = document.querySelectorAll('.o_data_row');
rows.forEach(row => {
const supplierCell = row.children[3]; // Dostawca jest w komórce 3
if (supplierCell) {
const supplierName = supplierCell.textContent.trim();
if (supplierName) {
suppliers.add(supplierName);
}
}
});
if (suppliers.size === 0) {
log('Brak dostawców do filtrowania');
return;
}
// Utwórz kontener filtra - będzie za przyciskami w lewym panelu
const filterContainer = document.createElement('div');
filterContainer.className = 'kfz-filter-container';
filterContainer.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
margin-left: 15px;
height: 32px;
`;
// Etykieta z ikoną
const label = document.createElement('span');
label.innerHTML = ' Dostawca:';
label.style.cssText = `
color: #6c757d;
font-size: 13px;
white-space: nowrap;
font-weight: normal;
`;
// Select z dostawcami - styl podobny do innych kontrolek Odoo
const supplierFilter = document.createElement('select');
supplierFilter.className = 'form-control';
supplierFilter.style.cssText = `
height: 32px;
padding: 4px 8px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 13px;
background-color: white;
min-width: 200px;
max-width: 250px;
line-height: 1.5;
`;
// Opcja "Wszyscy"
const allOption = document.createElement('option');
allOption.value = '';
allOption.textContent = 'Wszyscy dostawcy';
supplierFilter.appendChild(allOption);
// Opcje dla każdego dostawcy
Array.from(suppliers).sort().forEach(supplier => {
const option = document.createElement('option');
option.value = supplier;
option.textContent = supplier;
supplierFilter.appendChild(option);
});
// Event listener dla filtra dostawców
supplierFilter.addEventListener('change', () => {
filterBySupplier(supplierFilter.value);
});
filterContainer.appendChild(label);
filterContainer.appendChild(supplierFilter);
// Wstaw filtr za przyciskami w lewym panelu
leftPanel.parentNode.appendChild(filterContainer);
log('Dodano filtr dostawców za przyciskami w lewym panelu');
}
/**
* Fallback - dodaje filtry przed tabelą jeśli nie ma panelu wyszukiwania
*/
function addQuickFiltersBeforeTable() {
const table = document.querySelector('.o_list_table');
if (!table) return;
// Kontener dla filtrów (stary styl)
const filterContainer = document.createElement('div');
filterContainer.className = 'kfz-filter-container';
filterContainer.style.cssText = `
margin: 10px 0;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
display: flex;
gap: 10px;
align-items: center;
`;
// Filtr po dostawcy
const supplierFilter = document.createElement('select');
supplierFilter.style.cssText = 'padding: 5px; border-radius: 3px; border: 1px solid #ccc;';
supplierFilter.innerHTML = '';
// Pobierz unikalnych dostawców
const suppliers = new Set();
const rows = document.querySelectorAll('.o_data_row');
rows.forEach(row => {
const supplierCell = row.children[3];
if (supplierCell) {
const supplierName = supplierCell.textContent.trim();
if (supplierName) {
suppliers.add(supplierName);
}
}
});
suppliers.forEach(supplier => {
const option = document.createElement('option');
option.value = supplier;
option.textContent = supplier;
supplierFilter.appendChild(option);
});
// Event listener dla filtra dostawców
supplierFilter.addEventListener('change', () => {
filterBySupplier(supplierFilter.value);
});
filterContainer.appendChild(document.createTextNode('Filtruj: '));
filterContainer.appendChild(supplierFilter);
// Wstaw filtry przed tabelą
table.parentNode.insertBefore(filterContainer, table);
log('Dodano filtry przed tabelą (fallback)');
}
/**
* Filtruje wiersze według dostawcy
* @param {string} supplier - Nazwa dostawcy
*/
function filterBySupplier(supplier) {
const rows = document.querySelectorAll('.o_data_row');
rows.forEach(row => {
const supplierCell = row.children[3]; // Dostawca jest w komórce 3
if (!supplierCell) return;
const supplierName = supplierCell.textContent.trim();
const shouldShow = !supplier || supplierName === supplier;
row.style.display = shouldShow ? '' : 'none';
});
log(`Filtrowanie według dostawcy: ${supplier || 'wszyscy'}`);
}
/**
* Kopiuje tekst do schowka (jak w idea-orders.user.js)
* @param {string} text - Tekst do skopiowania
*/
async function tmCopyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch (error) {
// Fallback dla starszych przeglądarek
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
}
/**
* Czyni komórkę kopiowalną (styl z idea-orders.user.js)
* @param {Element} td - Komórka tabeli
*/
function makeCellCopyable(td) {
if (!td || td.dataset.tmCopyable) return;
td.dataset.tmCopyable = '1';
td.classList.add('tm-copyable');
td.addEventListener('click', async (e) => {
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
let val = (td.textContent || '').trim();
if (!val) return;
// Dla kwot usuń "zł" i zbędne spacje oraz znaki specjalne
if (val.includes('zł')) {
val = val.replace(/\s*zł\s*$/, '').trim();
// Usuń też znaki niełamliwych spacji i inne znaki formatujące
val = val.replace(/[\u00A0\u202F\u2009]/g, ' ').trim();
}
try {
await tmCopyToClipboard(val);
const old = td.title;
td.title = `Skopiowano: ${val}`;
td.classList.add('tm-copied');
setTimeout(() => {
td.title = old || 'Skopiowano';
td.classList.remove('tm-copied');
}, 1200);
log(`Skopiowano: ${val}`);
} catch (error) {
console.error('[Tampermonkey] Błąd podczas kopiowania komórki:', error);
td.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
/**
* Dodaje funkcjonalność kopiowania do wybranych kolumn
*/
function addCopyFunctionality() {
// Znajdź wszystkie wiersze danych
const dataRows = document.querySelectorAll('.o_data_row');
log(`Znaleziono ${dataRows.length} wierszy danych`);
if (dataRows.length === 0) {
log('Brak wierszy danych - sprawdzam strukturę tabeli');
const allRows = document.querySelectorAll('tr');
log(`Wszystkich wierszy: ${allRows.length}`);
allRows.forEach((row, index) => {
if (index < 5) { // Pokaż tylko pierwsze 5
log(`Wiersz ${index}: klasy="${row.className}", komórek=${row.children.length}`);
}
});
}
dataRows.forEach((row, rowIndex) => {
log(`Przetwarzam wiersz ${rowIndex}`);
// Debuguj wszystkie komórki w wierszu (tylko pierwszy wiersz żeby nie spamować)
if (rowIndex === 0) {
log(`=== DEBUGOWANIE STRUKTURY PIERWSZEGO WIERSZA ===`);
Array.from(row.children).forEach((cell, cellIndex) => {
const dataName = cell.getAttribute('data-name');
const text = cell.textContent.trim();
const classes = cell.className;
log(` Komórka ${cellIndex}: data-name="${dataName}", klasy="${classes}", tekst="${text.substring(0, 40)}"`);
});
log(`=== KONIEC DEBUGOWANIA STRUKTURY ===`);
}
// Numer faktury - komórka 2 (na podstawie debugowania)
let numberCell = row.children[2];
if (numberCell && numberCell.textContent.includes('KFZ/')) {
makeCellCopyable(numberCell);
log(`✓ Dodano kopiowanie dla numeru: "${numberCell.textContent.trim()}"`);
} else if (numberCell) {
log(`Pominięto kopiowanie numeru: "${numberCell.textContent.trim()}" (brak KFZ/)`);
} else {
log(`Nie znaleziono komórki numeru (indeks 2)`);
}
// Data faktury - komórka 4 (na podstawie debugowania)
let dateCell = row.children[4];
if (dateCell && dateCell.textContent.match(/\d{2}\.\d{2}\.\d{4}/)) {
makeCellCopyable(dateCell);
log(`✓ Dodano kopiowanie dla daty: "${dateCell.textContent.trim()}"`);
} else if (dateCell) {
log(`Pominięto kopiowanie daty: "${dateCell.textContent.trim()}" (nie pasuje format)`);
} else {
log(`Nie znaleziono komórki daty (indeks 4)`);
}
// Dokument źródłowy - komórka 5 (na podstawie debugowania: "ZZ/00310")
let invoiceOriginCell = row.children[5];
if (invoiceOriginCell) {
const invoiceOriginText = invoiceOriginCell.textContent.trim();
log(`Znaleziono dokument źródłowy: "${invoiceOriginText}"`);
if (invoiceOriginText && invoiceOriginText !== '' && invoiceOriginText !== '-') {
makeCellCopyable(invoiceOriginCell);
log(`✓ Dodano kopiowanie dla dokumentu źródłowego: "${invoiceOriginText}"`);
} else {
log(`Dokument źródłowy pusty lub myślnik: "${invoiceOriginText}"`);
}
} else {
log(`❌ Nie znaleziono komórki dokumentu źródłowego (indeks 5)`);
}
// Odnośnik - komórka 6 (na podstawie debugowania: "KFAS/2025/10/269/PL")
let refCell = row.children[6];
if (refCell) {
const refText = refCell.textContent.trim();
log(`Znaleziono odnośnik: "${refText}"`);
if (refText && refText !== '' && refText !== '-') {
makeCellCopyable(refCell);
log(`✓ Dodano kopiowanie dla odnośnika: "${refText}"`);
} else {
log(`Odnośnik pusty lub myślnik: "${refText}"`);
}
} else {
log(`❌ Nie znaleziono komórki odnośnika (indeks 6)`);
}
// Należność - komórka 10 (na podstawie debugowania: "1 123,80 zł")
let amountCell = row.children[10];
if (amountCell) {
const amountText = amountCell.textContent.trim();
log(`Znaleziono należność: "${amountText}"`);
if (amountText.includes('zł')) {
makeCellCopyable(amountCell);
log(`✓ Dodano kopiowanie dla należności: "${amountText}"`);
} else {
log(`Należność bez 'zł': "${amountText}"`);
}
} else {
log(`❌ Nie znaleziono komórki należności (indeks 10)`);
}
// Na podstawie - komórka 11 (na podstawie debugowania: "KFZ/283/09/2025 (KFAS/2025/09/799/PL)")
let basisCell = row.children[11];
if (basisCell) {
const basisText = basisCell.textContent.trim();
log(`Znaleziono "Na podstawie": "${basisText}"`);
if (basisText && basisText !== '' && basisText !== '-') {
makeCellCopyable(basisCell);
log(`✓ Dodano kopiowanie dla "Na podstawie": "${basisText}"`);
} else {
log(`Na podstawie puste lub myślnik: "${basisText}"`);
}
} else {
log(`❌ Nie znaleziono komórki "Na podstawie" (indeks 11)`);
}
});
log('Zakończono dodawanie funkcjonalności kopiowania');
}
/**
* Dodaje style CSS dla funkcjonalności kopiowania (dokładnie jak w idea-orders.user.js)
*/
function addCopyStyles() {
// Sprawdź czy style już zostały dodane
if (document.querySelector('#kfz-copy-styles')) {
log('Style kopiowania już istnieją - pomijam dodawanie');
return;
}
const style = document.createElement('style');
style.id = 'kfz-copy-styles';
style.textContent = `
/* Kopiowanie – ikona tuż za liczbą (inline), mniejsza, pomarańczowa */
td.tm-copyable { cursor: copy; }
td.tm-copyable::after {
content: '⧉'; /* symbol kopiowania w kolorze CSS */
display: inline-block; /* w tej samej linii co liczba */
margin-left: 6px; /* mały odstęp od liczby */
font-size: 12px; /* mniejszy rozmiar */
line-height: 1;
color: #ff9800; /* pomarańczowy */
opacity: .75; /* widoczna także bez hovera */
vertical-align: baseline;
pointer-events: none;
transition: opacity .15s ease;
}
td.tm-copyable:hover::after { opacity: 1; }
td.tm-copyable.tm-copied {
outline: 2px solid #ff9800;
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
}
`;
document.head.appendChild(style);
}
/**
* Czeka na załadowanie elementu
* @param {string} selector - Selektor CSS
* @param {number} timeout - Timeout w ms
* @returns {Promise}
*/
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} nie został znaleziony w czasie ${timeout}ms`));
}, timeout);
});
}
// Przechwytywanie żądań AJAX do search_read
function interceptAjaxRequests() {
const originalFetch = window.fetch;
window.fetch = function(...args) {
const promise = originalFetch.apply(this, args);
// Sprawdź czy to żądanie do search_read
if (args[0] && args[0].includes('/web/dataset/search_read')) {
promise.then(response => {
if (response.ok) {
log('Wykryto żądanie AJAX search_read - uruchamiam skrypt po opóźnieniu');
setTimeout(initEnhancements, 1500); // Większe opóźnienie dla AJAX
}
});
}
return promise;
};
// Przechwytywanie XMLHttpRequest (backup)
const originalXHR = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function(method, url, ...args) {
if (url && url.includes('/web/dataset/search_read')) {
this.addEventListener('load', () => {
log('Wykryto XHR search_read - uruchamiam skrypt po opóźnieniu');
setTimeout(initEnhancements, 1500);
});
}
return originalXHR.call(this, method, url, ...args);
};
}
// Inicjalizacja skryptu z obserwacją zmian
function startScript() {
log('🚀 SKRYPT ZAŁADOWANY - WERSJA 3.0');
log(`📍 URL: ${window.location.href}`);
// Przechwytuj żądania AJAX
interceptAjaxRequests();
// Pierwsze uruchomienie
setTimeout(initEnhancements, 1000);
// Obserwuj zmiany w DOM (dla dynamicznych stron)
const observer = new MutationObserver((mutations) => {
// Sprawdź czy dodano nowe wiersze danych
const hasNewDataRows = mutations.some(mutation =>
Array.from(mutation.addedNodes).some(node =>
node.nodeType === 1 && (
node.classList?.contains('o_data_row') ||
node.querySelector?.('.o_data_row')
)
)
);
if (hasNewDataRows) {
log('Wykryto nowe wiersze danych - uruchamiam skrypt');
setTimeout(initEnhancements, 800);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Obserwuj zmiany URL (hash) - kilka metod dla pewności
let lastUrl = window.location.href;
// 1. Standardowy hashchange
window.addEventListener('hashchange', () => {
log('Wykryto hashchange - resetuję stan i uruchamiam skrypt');
// Resetuj stan przy zmianie URL
const scriptMarker = document.querySelector('.kfz-script-initialized');
if (scriptMarker) {
scriptMarker.remove();
log('Usunięto marker inicjalizacji przy hashchange');
}
setTimeout(initEnhancements, 500);
setTimeout(initEnhancements, 1500); // Backup po dłuższym czasie
});
// 2. Obserwuj zmiany URL przez polling
setInterval(() => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
log(`URL zmieniony z ${lastUrl} na ${currentUrl}`);
lastUrl = currentUrl;
// Resetuj stan przy zmianie URL
const scriptMarker = document.querySelector('.kfz-script-initialized');
if (scriptMarker) {
scriptMarker.remove();
log('Usunięto marker inicjalizacji przy zmianie URL');
}
setTimeout(initEnhancements, 300);
setTimeout(initEnhancements, 1000);
}
}, 500);
// 3. Obserwuj zmiany w tytule strony (Odoo często zmienia tytuł)
let lastTitle = document.title;
const titleObserver = new MutationObserver(() => {
if (document.title !== lastTitle) {
log(`Tytuł zmieniony z "${lastTitle}" na "${document.title}"`);
lastTitle = document.title;
setTimeout(initEnhancements, 400);
}
});
titleObserver.observe(document.querySelector('title') || document.head, {
childList: true,
characterData: true,
subtree: true
});
// 4. Obserwuj zmiany w breadcrumb (ścieżka nawigacji Odoo)
const breadcrumbObserver = new MutationObserver(() => {
log('Wykryto zmiany w breadcrumb - sprawdzam czy to strona KFZ');
setTimeout(initEnhancements, 200);
});
// Obserwuj breadcrumb jeśli istnieje
const breadcrumbElement = document.querySelector('.breadcrumb, .o_cp_controller');
if (breadcrumbElement) {
breadcrumbObserver.observe(breadcrumbElement, {
childList: true,
characterData: true,
subtree: true
});
}
// 5. Dodatkowe sprawdzenie co 3 sekundy (backup)
setInterval(() => {
const url = window.location.href;
const isAccountMoveList = url.includes('model=account.move') && url.includes('view_type=list');
const hasFilter = document.querySelector('.kfz-filter-container');
const breadcrumbCheck = document.querySelector('.breadcrumb');
const hasCorrectionsBreadcrumb = breadcrumbCheck && breadcrumbCheck.textContent.includes('Korekty');
if (isAccountMoveList && !hasFilter && (isKfzListingPage() || hasCorrectionsBreadcrumb)) {
log('Backup check - wykryto stronę account.move bez filtra, uruchamiam skrypt');
initEnhancements();
}
}, 3000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startScript);
} else {
startScript();
}
})();