Initial commit

This commit is contained in:
Adam Grodecki 2025-12-15 19:42:21 +01:00
commit 0515809c53
15 changed files with 11103 additions and 0 deletions

97
.rules Normal file
View file

@ -0,0 +1,97 @@
# ===========================
# Tampermonkey zasady ogólne
# ===========================
- file-type: "Zawsze generuj userscript jako plik .user.js"
- header-block: |
Każdy skrypt musi zaczynać się od pełnego bloku nagłówka Tampermonkey, np.:
// ==UserScript==
// @name Example Script
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Krótki opis działania skryptu
// @author Adam
// @match https://example.com/*
// @icon https://example.com/favicon.ico
// @grant none
// @run-at document-end
// ==/UserScript==
- comments: "Dodawaj komentarze do głównych sekcji skryptu"
# ===========================
# Styl kodu
# ===========================
- naming:
variables: "camelCase"
functions: "camelCase"
classes: "PascalCase"
- prefer-use: "const i let zamiast var"
- async: "Preferuj async/await zamiast then()"
- strict-mode: "Zawsze używaj 'use strict'; na początku"
- formatting: "Stosuj ESLint/Prettier style (2 spacje, średnik na końcu)"
# ===========================
# Struktura userscriptu
# ===========================
- main-entry: "Opakuj kod w IIFE (() => { ... }) aby uniknąć globalnych kolizji"
- dom-ready: "Stosuj document-end lub waitForElement zamiast ręcznych timeoutów"
- logging: "Używaj console.log tylko w trybie debug, dodając [Tampermonkey] jako prefix"
# ===========================
# Najlepsze praktyki
# ===========================
- selectors: "Preferuj querySelector/querySelectorAll zamiast jQuery"
- mutation-observer: "Do dynamicznych stron używaj MutationObserver zamiast setInterval"
- storage: "Używaj GM_setValue i GM_getValue do przechowywania ustawień"
- security: "Unikaj eval() i dynamicznego wykonywania kodu"
- updates: "Dodaj @updateURL i @downloadURL jeśli skrypt ma być hostowany"
# ===========================
# Testy i debug
# ===========================
- debug-flag: "Dodaj stałą DEBUG = true/false i używaj jej w logach"
- error-handling: "Każdy główny blok async powinien mieć try/catch"
# ===========================
# Wersja i opisy
# ===========================
- po każdej zmianie podnieś numer wersji, np. z 3.0 na 3.1
- każda zmiana kodu (dodanie funkcji, poprawka błędu, refaktoryzacja) MUSI kończyć się zmianą wersji
- nawet najmniejsze poprawki wymagają podwyższenia numeru wersji
- po każdej dodanej funkcjonalności opisz to w sekcji OPIS
- po każdej zmienie dodaj to, co zostało zrobione w danej wersji
# ===========================
# Ustawienia dodatkowe
# ===========================
- odpowiadaj zawsze w języku polskim
- przy wyszukiwaniu kolumn w tabelach zawsze używać nazwy kolumny (atrybut title, data-name lub textContent) zamiast indeksu kolumny
- indeksy kolumn mogą się zmieniać, ale nazwy pozostają stabilne
- funkcja findColumnByName powinna sprawdzać kolejno: title, data-name, textContent
# ===========================
# Nagłówek userscriptu - wymagane pola
# ===========================
- icon: "Zawsze dodawaj @icon z favicon strony docelowej"
- downloadURL: "Dodaj @downloadURL dla automatycznych aktualizacji"
- updateURL: "Dodaj @updateURL dla automatycznych aktualizacji"
- grants: "Dodaj odpowiednie @grant dla używanych funkcji GM_*"
# ===========================
# Dokumentacja w skrypcie
# ===========================
- opis-sekcja: |
Każdy skrypt musi mieć sekcję OPIS z:
- Wymaganymi parametrami URL
- Wymaganymi kolumnami w tabeli
- Opisem działania skryptu
- Listą funkcji kopiowania/przetwarzania
- Informacjami o zapamiętywaniu ustawień
- Informacjami o automatycznych aktualizacjach
- changelog-sekcja: |
Każdy skrypt musi mieć sekcję CHANGELOG z:
- Numerem wersji i datą
- Szczegółowym opisem zmian
- Informacją o podwyższeniu numeru wersji
- Format: "vX.Y (YYYY) - NAZWA ZMIANY"

View file

@ -0,0 +1,475 @@
// ==UserScript==
// @name Allegro Sales Center - Koszt 1 szt Calculator
// @namespace http://tampermonkey.net/
// @version 1.3.0
// @description Adds a sortable "Koszt 1 szt" column to Allegro Sales Center statistics table that calculates cost per sold item
// @author Emma
// @match https://salescenter.allegro.com/ads/panel/stats/ads?marketplaceId=allegro-pl
// @icon https://www.google.com/s2/favicons?sz=64&domain=allegro.com
// @downloadURL https://n8n.emma.net.pl/webhook/allegro-stat
// @updateURL https://n8n.emma.net.pl/webhook/allegro-stat
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
/**
* Parses Polish currency string to number
* @param {string} value - Currency string (e.g., "1842,53 zł")
* @returns {number} - Parsed number value
*/
const parseCurrency = (value) => {
if (!value || typeof value !== 'string') return 0;
const cleaned = value.replace(/\s+zł/g, '').replace(/\s+/g, '').replace(',', '.');
return parseFloat(cleaned) || 0;
};
/**
* Parses integer string to number
* @param {string} value - Integer string (e.g., "1 002")
* @returns {number} - Parsed integer value
*/
const parseInteger = (value) => {
if (!value || typeof value !== 'string') return 0;
const cleaned = value.replace(/\s+/g, '');
return parseInt(cleaned, 10) || 0;
};
/**
* Formats number to Polish currency format
* @param {number} value - Number to format
* @returns {string} - Formatted currency string
*/
const formatCurrency = (value) => {
if (isNaN(value) || !isFinite(value)) return '0,00 zł';
return value.toFixed(2).replace('.', ',') + ' zł';
};
/**
* Finds the index of a table header by its text content
* @param {NodeList} headers - List of header elements
* @param {string} text - Text to search for
* @returns {number} - Index of the header or -1 if not found
*/
const findHeaderIndex = (headers, text) => {
for (let i = 0; i < headers.length; i++) {
if (headers[i].textContent.trim() === text) {
return i;
}
}
return -1;
};
/**
* Calculates cost per item (Koszt / Sprzedane sztuki)
* @param {number} totalCost - Total cost value
* @param {number} soldItems - Number of sold items
* @returns {number} - Cost per item rounded to 2 decimal places
*/
const calculateCostPerItem = (totalCost, soldItems) => {
if (soldItems === 0) return 0;
return Math.round((totalCost / soldItems) * 100) / 100;
};
/**
* Disables other column sorting and hides their sort icons
*/
const disableOtherSorting = () => {
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (!scrollableTable) return;
const headers = scrollableTable.querySelectorAll('thead th:not([data-custom-column="koszt-1-szt"])');
headers.forEach(header => {
header.style.pointerEvents = 'none';
header.style.opacity = '0.5';
// Hide sort icons in other columns
const sortIcon = header.querySelector('.Table-module__table-header__sort-icon__Kx7KdyE');
if (sortIcon) {
sortIcon.style.display = 'none';
sortIcon.setAttribute('data-was-visible', 'true');
}
});
};
/**
* Enables other column sorting and restores their sort icons
*/
const enableOtherSorting = () => {
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (!scrollableTable) return;
const headers = scrollableTable.querySelectorAll('thead th:not([data-custom-column="koszt-1-szt"])');
headers.forEach(header => {
header.style.pointerEvents = '';
header.style.opacity = '';
// Restore sort icons in other columns
const sortIcon = header.querySelector('.Table-module__table-header__sort-icon__Kx7KdyE');
if (sortIcon && sortIcon.getAttribute('data-was-visible') === 'true') {
sortIcon.style.display = '';
sortIcon.removeAttribute('data-was-visible');
}
});
};
/**
* Sorts table rows by cost per item
* @param {string} direction - 'asc' or 'desc'
*/
const sortTableByCosztPerItem = (direction) => {
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (!scrollableTable) return;
const tbody = scrollableTable.querySelector('tbody');
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr'));
// Separate summary row (first row) from data rows
const summaryRow = rows[0]?.classList.contains('Table-module__table-summary__ayBWA2V') ? rows[0] : null;
const dataRows = summaryRow ? rows.slice(1) : rows;
// Sort data rows
dataRows.sort((rowA, rowB) => {
const cellA = rowA.querySelector('td[data-custom-column="koszt-1-szt"]');
const cellB = rowB.querySelector('td[data-custom-column="koszt-1-szt"]');
if (!cellA || !cellB) return 0;
const valueA = parseCurrency(cellA.textContent);
const valueB = parseCurrency(cellB.textContent);
if (direction === 'asc') {
return valueA - valueB;
} else {
return valueB - valueA;
}
});
// Clear tbody
tbody.innerHTML = '';
// Re-add rows
if (summaryRow) {
tbody.appendChild(summaryRow);
}
dataRows.forEach(row => tbody.appendChild(row));
console.log(`Table sorted by Koszt 1 szt (${direction})`);
};
/**
* Adds the "Koszt 1 szt" column to the table
*/
const addCostPerItemColumn = () => {
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (!scrollableTable) {
console.log('Table not found, retrying...');
return false;
}
const thead = scrollableTable.querySelector('thead tr');
const tbody = scrollableTable.querySelector('tbody');
if (!thead || !tbody) {
console.log('Table structure not found');
return false;
}
const headers = thead.querySelectorAll('th');
const roasIndex = findHeaderIndex(headers, 'ROAS');
if (roasIndex === -1) {
console.log('ROAS column not found');
return false;
}
// Check if column already exists
let existingHeader = scrollableTable.querySelector('thead th[data-custom-column="koszt-1-szt"]');
if (!existingHeader) {
// Add new header after ROAS
const newHeader = document.createElement('th');
newHeader.className = 'Table-module__table-header__cpMic9I Utils-module__cursorPointer__ugRHHx8';
newHeader.setAttribute('data-custom-column', 'koszt-1-szt');
newHeader.setAttribute('data-sort-direction', 'none');
// Create header content with tooltip
const headerContent = document.createElement('div');
headerContent.style.display = 'flex';
headerContent.style.alignItems = 'center';
headerContent.style.justifyContent = 'space-between';
headerContent.style.width = '100%';
const headerText = document.createElement('span');
headerText.textContent = 'Koszt 1 szt';
headerText.style.flex = '1';
// Copy tooltip implementation from other columns by examining existing headers
const currentExistingHeaders = scrollableTable.querySelectorAll('thead th');
if (currentExistingHeaders.length > 0) {
// Copy classes from the first existing header
const firstHeader = currentExistingHeaders[0];
const classes = firstHeader.className;
newHeader.className = classes;
// Copy exact styles from the header itself, not its children
const computedStyle = window.getComputedStyle(firstHeader);
headerText.style.fontSize = computedStyle.fontSize;
headerText.style.fontFamily = computedStyle.fontFamily;
headerText.style.fontWeight = computedStyle.fontWeight;
headerText.style.lineHeight = computedStyle.lineHeight;
headerText.style.color = computedStyle.color;
headerText.style.fontStyle = computedStyle.fontStyle;
headerText.style.textTransform = computedStyle.textTransform;
headerText.style.letterSpacing = computedStyle.letterSpacing;
}
// Create custom tooltip that appears above the element
const tooltipText = 'Wyliczenie kosztów reklamy jednej sprzedanej sztuki. Podzielono Koszt przez sprzedane sztuki.\n\nAby przesortować kolumnę naciśnij raz aby sortować od największej do namniejszej wartości, naciśnij drugi raz aby sortować odwrotnie a trzeci raz aby inne kolumny miały ponownie możliwość sortowania.';
// Add mouse events for custom tooltip
let tooltipDiv = null;
newHeader.addEventListener('mouseenter', (e) => {
// Create tooltip div
tooltipDiv = document.createElement('div');
tooltipDiv.textContent = tooltipText;
tooltipDiv.style.position = 'fixed';
tooltipDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
tooltipDiv.style.color = 'white';
tooltipDiv.style.padding = '8px 12px';
tooltipDiv.style.borderRadius = '4px';
tooltipDiv.style.fontSize = '12px';
tooltipDiv.style.whiteSpace = 'pre-line';
tooltipDiv.style.zIndex = '9999';
tooltipDiv.style.maxWidth = '300px';
tooltipDiv.style.pointerEvents = 'none';
// Position above the element
const rect = newHeader.getBoundingClientRect();
tooltipDiv.style.left = (rect.left + rect.width / 2 - tooltipDiv.offsetWidth / 2) + 'px';
tooltipDiv.style.top = (rect.top - tooltipDiv.offsetHeight - 5) + 'px';
document.body.appendChild(tooltipDiv);
});
newHeader.addEventListener('mouseleave', () => {
if (tooltipDiv) {
tooltipDiv.remove();
tooltipDiv = null;
}
});
// Create sort icon container (hidden by default)
const sortIconContainer = document.createElement('div');
sortIconContainer.className = 'Table-module__table-header__sort-icon__Kx7KdyE';
sortIconContainer.style.display = 'none';
sortIconContainer.setAttribute('data-sort-icon', 'true');
// Create SVG arrow (like Allegro original)
const arrowDiv = document.createElement('div');
arrowDiv.className = 'mp7g_oh m0s5_e6 ArrowIcon-module__arrowDown__HSH8I9k';
arrowDiv.style.width = '22px';
arrowDiv.style.height = '22px';
arrowDiv.setAttribute('title', 'Sortuj');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'mp7g_f6 m7er_k4 mse2_k4 mg9e_0 mvrt_0 mj7a_0 mh36_0 mq1m_0 mj7u_0');
svg.style.width = '22px';
svg.style.height = '22px';
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', 'https://a.allegroimg.com/original/3424ea/a70a9cfd4ee59ddb6d4fc30364c7/action-common-arrowhead-c3c511fba9');
image.setAttribute('href', 'https://a.allegroimg.com/original/3424ea/a70a9cfd4ee59ddb6d4fc30364c7/action-common-arrowhead-c3c511fba9');
image.setAttribute('width', '22px');
image.setAttribute('height', '22px');
image.setAttribute('data-testid', 'svg-image-element');
svg.appendChild(image);
arrowDiv.appendChild(svg);
sortIconContainer.appendChild(arrowDiv);
headerContent.appendChild(headerText);
headerContent.appendChild(sortIconContainer);
newHeader.appendChild(headerContent);
// Add click handler for sorting
newHeader.addEventListener('click', () => {
const currentDirection = newHeader.getAttribute('data-sort-direction');
let newDirection = 'desc';
if (currentDirection === 'none') {
// First click - sort descending (arrow pointing down)
newDirection = 'desc';
sortIconContainer.style.display = 'block';
arrowDiv.style.transform = 'rotate(90deg)';
disableOtherSorting();
} else if (currentDirection === 'desc') {
// Second click - sort ascending (arrow pointing up)
newDirection = 'asc';
arrowDiv.style.transform = 'rotate(-90deg)';
} else {
// Third click - reset sorting and restore original order
newDirection = 'none';
sortIconContainer.style.display = 'none';
enableOtherSorting();
newHeader.setAttribute('data-sort-direction', 'none');
return;
}
newHeader.setAttribute('data-sort-direction', newDirection);
sortTableByCosztPerItem(newDirection);
});
if (roasIndex + 1 < headers.length) {
headers[roasIndex + 1].before(newHeader);
} else {
thead.appendChild(newHeader);
}
existingHeader = newHeader;
}
// Process data cells for each row
const rows = tbody.querySelectorAll('tr');
rows.forEach((row) => {
// Check if this row already has our custom column
const existingCell = row.querySelector('td[data-custom-column="koszt-1-szt"]');
if (existingCell) {
// Remove it to recalculate with correct indices
existingCell.remove();
}
const cells = row.querySelectorAll('td');
// Get Koszt value (index 4 in scrollable table: 0-Kliknięcia, 1-Odsłony, 2-CTR, 3-Średnie CPC, 4-Koszt)
const kosztCell = cells[4];
// Get Sprzedane sztuki value (index 7 in scrollable table: 5-ROAS, 6-Zainteresowanie, 7-Sprzedane sztuki)
const sprzedaneCell = cells[7];
let costPerItemValue = '0,00 zł';
if (kosztCell && sprzedaneCell) {
const kosztText = kosztCell.textContent.trim();
// Handle button elements in sprzedane sztuki column
const sprzedaneButton = sprzedaneCell.querySelector('button');
const sprzedaneText = sprzedaneButton ? sprzedaneButton.textContent.trim() : sprzedaneCell.textContent.trim();
const totalCost = parseCurrency(kosztText);
const soldItems = parseInteger(sprzedaneText);
const costPerItem = calculateCostPerItem(totalCost, soldItems);
costPerItemValue = formatCurrency(costPerItem);
}
const newCell = document.createElement('td');
newCell.className = 'StatisticsTable-module__statistics-table__cell__x1ILnxF Table-module__table-cell__LI2CMBo Table-module__table-cell--align-right__kyh17WP';
newCell.textContent = costPerItemValue;
newCell.setAttribute('data-custom-column', 'koszt-1-szt');
// Find ROAS cell again after removal
const currentCells = row.querySelectorAll('td');
const currentRoasIndex = 5; // ROAS is at index 5 in scrollable table
if (currentRoasIndex + 1 < currentCells.length) {
currentCells[currentRoasIndex + 1].before(newCell);
} else {
row.appendChild(newCell);
}
});
console.log('Koszt 1 szt column added successfully');
return true;
};
/**
* Observes DOM changes and adds the column when table is loaded
*/
const observeTableChanges = () => {
const targetNode = document.body;
const config = { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-selected'] };
let isProcessing = false;
let processingTimeout = null;
const callback = (mutationsList, observer) => {
// Avoid infinite loop - skip if already processing
if (isProcessing) return;
// Check for tab changes or table additions
for (const mutation of mutationsList) {
// Check for tab panel attribute changes (tab switching)
if (mutation.type === 'attributes' && mutation.attributeName === 'aria-selected') {
const target = mutation.target;
if (target.getAttribute('aria-selected') === 'true' && target.getAttribute('role') === 'tabpanel') {
console.log('Tab switched, waiting for table to load...');
isProcessing = true;
// Clear previous timeout
if (processingTimeout) {
clearTimeout(processingTimeout);
}
// Wait for table to load after tab switch
processingTimeout = setTimeout(() => {
addCostPerItemColumn();
isProcessing = false;
}, 800);
break;
}
}
// Check for table additions
if (mutation.type === 'childList') {
const tableExists = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (tableExists) {
isProcessing = true;
// Clear previous timeout
if (processingTimeout) {
clearTimeout(processingTimeout);
}
processingTimeout = setTimeout(() => {
addCostPerItemColumn();
isProcessing = false;
}, 500);
break;
}
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
};
/**
* Initializes the script
*/
const init = () => {
console.log('Allegro Sales Center - Koszt 1 szt Calculator initialized');
// Try to add column immediately
setTimeout(() => {
addCostPerItemColumn();
}, 2000);
// Observe for future changes (pagination, filters, etc.)
observeTableChanges();
};
// Start the script
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

318
idea/idea-edi-tim.user.js Normal file
View file

@ -0,0 +1,318 @@
// ==UserScript==
// @name IDEAERP - Dodaj sekcję EMMA z TIM EDI
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Dodaje sekcję "EMMA" do menu głównego z funkcjonalnością generowania plików EDI dla TIM. Po kliknięciu otwiera się modal z polem do wprowadzenia numeru zamówienia i wysyłania danych do serwera TIM EDI. Automatyczna konwersja liter na wielkie.
// @match https://emma.ideaerp.pl/web*
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
// @downloadURL https://n8n.emma.net.pl/webhook/edi-tim
// @updateURL https://n8n.emma.net.pl/webhook/edi-tim
// @grant none
// ==/UserScript==
/* ========================OPIS==============================
WERSJA 1.0 - Integracja z TIM EDI + Automatyczne aktualizacje
CEL SKRYPTU:
- Dodanie sekcji "EMMA" do menu głównego IDEAERP
- Integracja z systemem TIM EDI do generowania plików
- Umożliwienie wysyłania numerów zamówień do serwera TIM
DZIAŁANIE SKRYPTU:
1. Lokalizuje sekcję menu o ID "209" w systemie IDEAERP
2. Tworzy nową sekcję "EMMA" z elementem "Tim Edi"
3. Po kliknięciu otwiera modal z formularzem
4. Umożliwia wprowadzenie numeru zamówienia
5. Wysyła dane do serwera TIM EDI (https://192.168.0.97:5011/tim/edi)
6. Czyści formularz po udanym wysłaniu
WYMAGANIA TECHNICZNE:
- Dostęp do serwera TIM EDI (192.168.0.97:5011)
- Sekcja menu o data-menu-section="209" w IDEAERP
- Obsługa fetch API w przeglądarce
- Dostęp do serwera aktualizacji (n8n.emma.net.pl/webhook/edi-tim)
- Tampermonkey z obsługą automatycznych aktualizacji
FUNKCJONALNOŚCI:
+ Dodawanie sekcji EMMA do menu głównego
+ Modal z formularzem do wprowadzania numeru zamówienia
+ Automatyczna konwersja liter na wielkie (w czasie rzeczywistym)
+ Zabezpieczenie przed wklejaniem małych liter
+ Walidacja wprowadzonych danych
+ Wysyłanie POST request do serwera TIM EDI
+ Obsługa błędów sieciowych
+ Czyszczenie formularza po wysłaniu
+ Przycisk zamknięcia modala (X)
+ Overlay z efektem przyciemnienia tła
STRUKTURA MODALA:
- Tytuł: "Generuj plik EDI"
- Pole tekstowe z placeholder: "Podaj numer zamówienia"
- Przycisk "Wyślij" (niebieski)
- Przycisk zamknięcia (X w prawym górnym rogu)
- Overlay z półprzezroczystym tłem
ENDPOINT API:
- URL: https://192.168.0.97:5011/tim/edi
- Method: POST
- Content-Type: application/json
- Body: { "Zamówienie": "numer_zamówienia" }
OBSŁUGA BŁĘDÓW:
- Walidacja pustego pola (alert: "Dodaj zamówienia")
- Obsługa błędów sieciowych (alert: "Wystąpił błąd podczas wysyłania danych")
- Logowanie błędów do konsoli
INTEGRACJA Z IDEAERP:
- Wstrzykiwanie sekcji do istniejącego menu
- Umieszczenie sekcji EMMA jako pierwszy element w kontenerze
- Zachowanie stylów i struktury menu IDEAERP
- Obsługa dynamicznego ładowania strony
AUTOMATYCZNE AKTUALIZACJE:
- Serwer aktualizacji: https://n8n.emma.net.pl/webhook/edi-tim
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
- Jednoklikowa aktualizacja bez konieczności ręcznego pobierania
- Zachowanie ustawień użytkownika podczas aktualizacji
- Kompatybilność z systemem n8n do zarządzania wersjami
CHANGELOG:
v1.0 (2024) - AUTOMATYCZNE AKTUALIZACJE
- Dodano @downloadURL i @updateURL wskazujące na n8n.emma.net.pl/webhook/edi-tim
- Integracja z systemem n8n do zarządzania wersjami skryptu
- Automatyczne sprawdzanie i pobieranie aktualizacji przez Tampermonkey
- Dokumentacja procesu aktualizacji w sekcji OPIS
- Podwyższenie numeru wersji do 1.0 (stabilna wersja)
v0.9 (2024) - AUTOMATYCZNA KONWERSJA LITER
- Dodano automatyczną konwersję liter na wielkie w czasie rzeczywistym
- Zabezpieczenie przed wklejaniem małych liter
- Zachowanie pozycji kursora podczas konwersji
- Dodano CSS text-transform: uppercase dla wizualnej spójności
v0.8 (2024) - PIERWSZA WERSJA
- Dodanie sekcji EMMA do menu głównego
- Implementacja modala z formularzem
- Integracja z serwerem TIM EDI
- Obsługa wysyłania numerów zamówień
- Walidacja danych wejściowych
- Obsługa błędów sieciowych
- Czyszczenie formularza po wysłaniu
- Responsywny design modala
- Przycisk zamknięcia modala
============================================================ */
(function() {
'use strict';
// ——— Funkcja główna: dodawanie sekcji EMMA do menu ———
function addEmmaSection() {
var targetSection = document.querySelector('div.i_secondary_menu__section[data-menu-section="209"]');
if (!targetSection) {
setTimeout(addEmmaSection, 500);
return;
}
if (document.querySelector('div.i_secondary_menu__section[data-menu-section="1248"]')) {
attachPopupListener();
return;
}
var newSectionHtml = `
<div class="i_secondary_menu__section folded" data-menu-section="1248">
<a href="#" class="i_secondary_menu__toggle o_menu_header_lvl_1" data-menu-xmlid="ideaerp_base.i7_base_sale_order_folders_main_menu" data-display="static" role="button" aria-expanded="false">
EMMA
</a>
<ul class="i_secondary_menu__content" role="menu">
<li>
<a role="menuitem" href="#" class="dropdown-item o_menu_entry_lvl_2" id="timEdiLink">
<span>Tim Edi</span>
</a>
</li>
</ul>
</div>
`;
var tempDiv = document.createElement('div');
tempDiv.innerHTML = newSectionHtml.trim();
var newSection = tempDiv.firstElementChild;
// Wstawiamy sekcję jako pierwszy element w kontenerze
var parent = targetSection.parentNode;
parent.insertBefore(newSection, parent.firstChild);
console.log("Dodano sekcję EMMA z TIM EDI do menu jako pierwszy element.");
attachPopupListener();
}
// ——— Obsługa kliknięć: podpięcie event listenera do linku TIM EDI ———
function attachPopupListener() {
var timEdiLink = document.getElementById('timEdiLink');
if (timEdiLink) {
timEdiLink.addEventListener('click', function(e) {
e.preventDefault();
showPopup();
});
}
}
// ——— Tworzenie i wyświetlanie modala z formularzem TIM EDI ———
function showPopup() {
var overlay = document.getElementById('edi-popup-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'edi-popup-overlay';
Object.assign(overlay.style, {
position: 'fixed',
top: '0',
left: '0',
right: '0',
bottom: '0',
background: 'rgba(0, 0, 0, 0.6)',
zIndex: '10000',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
});
var modal = document.createElement('div');
modal.id = 'edi-popup';
Object.assign(modal.style, {
background: '#fff',
borderRadius: '8px',
padding: '20px',
width: '320px',
boxSizing: 'border-box',
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
position: 'relative',
fontFamily: 'Arial, sans-serif'
});
// ——— Przycisk zamknięcia modala (X w prawym górnym rogu) ———
var closeButton = document.createElement('button');
closeButton.id = 'edi-close';
closeButton.textContent = '×';
Object.assign(closeButton.style, {
position: 'absolute',
top: '1px',
right: '5px',
background: 'transparent',
border: 'none',
fontSize: '22px',
cursor: 'pointer',
color: '#888'
});
closeButton.addEventListener('click', function() {
overlay.style.display = 'none';
});
modal.appendChild(closeButton);
// ——— Tytuł modala ———
var title = document.createElement('h4');
title.textContent = 'Generuj plik EDI';
Object.assign(title.style, {
margin: '20px 0 15px 0',
fontSize: '16px',
color: '#888'
});
modal.appendChild(title);
// ——— Pole tekstowe do wprowadzania numeru zamówienia ———
var input = document.createElement('input');
input.type = 'text';
input.id = 'order-number';
input.placeholder = 'Podaj numer zamówienia';
Object.assign(input.style, {
width: '100%',
padding: '8px',
marginBottom: '15px',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '14px',
textTransform: 'uppercase' // Automatyczna konwersja na wielkie litery
});
// ——— Zabezpieczenie: automatyczna konwersja na wielkie litery ———
input.addEventListener('input', function(e) {
var cursorPosition = e.target.selectionStart;
var originalValue = e.target.value;
var upperValue = originalValue.toUpperCase();
// Aktualizujemy wartość tylko jeśli nastąpiła zmiana
if (originalValue !== upperValue) {
e.target.value = upperValue;
// Przywracamy pozycję kursora
e.target.setSelectionRange(cursorPosition, cursorPosition);
}
});
// ——— Dodatkowe zabezpieczenie dla wklejania tekstu ———
input.addEventListener('paste', function(e) {
setTimeout(function() {
var cursorPosition = input.selectionStart;
var originalValue = input.value;
var upperValue = originalValue.toUpperCase();
if (originalValue !== upperValue) {
input.value = upperValue;
input.setSelectionRange(cursorPosition, cursorPosition);
}
}, 0);
});
modal.appendChild(input);
// ——— Przycisk wysyłania danych do serwera TIM EDI ———
var button = document.createElement('button');
button.id = 'edi-submit';
button.textContent = 'Wyślij';
Object.assign(button.style, {
padding: '10px 20px',
fontSize: '14px',
background: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
});
button.addEventListener('click', function() {
// ——— Walidacja danych wejściowych ———
var orderNumber = input.value.trim();
if (!orderNumber) {
alert("Dodaj zamówienia");
return;
}
// ——— Wysyłanie danych do serwera TIM EDI ———
fetch('https://192.168.0.97:5011/tim/edi', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ "Zamówienie": orderNumber })
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// ——— Obsługa sukcesu: czyszczenie formularza i zamknięcie modala ———
console.log('Sukces:', data);
input.value = "";
overlay.style.display = 'none';
})
.catch((error) => {
// ——— Obsługa błędów sieciowych ———
console.error('Error:', error);
alert("Wystąpił błąd podczas wysyłania danych.");
});
});
modal.appendChild(button);
overlay.appendChild(modal);
document.body.appendChild(overlay);
} else {
overlay.style.display = 'flex';
}
}
// ——— Inicjalizacja: uruchomienie po załadowaniu strony ———
window.addEventListener('load', addEmmaSection);
})();

View file

@ -0,0 +1,276 @@
// ==UserScript==
// @name Idea Invoice Famica
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Rozszerza funkcjonalność faktur IdeaERP o dodawanie numeru EORI i ulepszone drukowanie/generowanie PDF dla faktur Famica Ltd.
// @author You
// @match https://emma.ideaerp.pl/my/invoices*
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
// @downloadURL https://n8n.emma.net.pl/webhook/invoice-famica
// @updateURL https://n8n.emma.net.pl/webhook/invoice-famica
// @grant none
// ==/UserScript==
/**
* IDEA INVOICE FAMICA - Skrypt Rozszerzający Przetwarzanie Faktur
*
* Ten skrypt użytkownika Tampermonkey rozszerza interfejs faktur IdeaERP poprzez
* dodanie trzech niestandardowych przycisków usprawniających workflow przetwarzania faktur.
*
* FUNKCJE:
* - Dodaje numer EORI (GB174521806000) do informacji o nabywcy na fakturach
* - Ulepszone drukowanie z prawidłową widocznością nagłówków tabel i cieniowania
* - Funkcjonalność generowania PDF z zachowaniem formatowania
*
* SZCZEGÓŁY TECHNICZNE:
* - Działa w systemie wyświetlania faktur opartym na iframe
* - Używa wstrzykiwania CSS do wymuszenia dostosowania kolorów przy drukowaniu
* - Implementuje obsługę MediaQueryList kompatybilną z różnymi przeglądarkami
* - Zapobiega duplikowaniu przycisków poprzez znaczniki dataset
*
* SPOSÓB UŻYCIA:
* 1. "Dodaj EORI" - Dodaje numer EORI do sekcji nabywcy na fakturze
* 2. "Wydrukuj z EORI" - Otwiera okno drukowania z ulepszonym formatowaniem
* 3. "Pobierz z EORI" - Prowadzi użytkownika przez zapisanie faktury jako PDF z formatowaniem
*
* HISTORIA ZMIAN:
* v0.2 (2025-09-24)
* - Dodano ulepszoną funkcjonalność drukowania z poprawkami widoczności nagłówków tabel
* - Zaimplementowano zachowanie cieniowania wierszy w wydruku
* - Dodano funkcjonalność pobierania PDF z instrukcją dla użytkownika
* - Naprawiono problemy kompatybilności MediaQueryList między przeglądarkami
* - Ulepszona obsługa błędów i mechanizmy czyszczenia
*
* v0.1 (2025-09-24)
* - Pierwsze wydanie z podstawową funkcjonalnością dodawania EORI
* - Podstawowe tworzenie przycisków i manipulacja zawartością iframe
* - Implementacja MutationObserver do wykrywania dynamicznej zawartości
*/
(function() {
'use strict';
function addButtons() {
const targetDivs = document.querySelectorAll('.o_download_pdf.btn-toolbar');
targetDivs.forEach(targetDiv => {
// Check if buttons are already added
if (targetDiv.dataset.customButtonsAdded) {
return;
}
// Create "Dodaj EORI" button
const eoriButtonGroup = document.createElement('div');
eoriButtonGroup.className = 'btn-group flex-grow-1 mr-1 mb-1';
const eoriButton = document.createElement('a');
eoriButton.className = 'btn btn-secondary btn-block';
eoriButton.href = '#';
eoriButton.innerHTML = '<i class="fa fa-plus"></i> Dodaj EORI';
eoriButton.onclick = function(e) {
e.preventDefault();
const iframe = document.getElementById('invoice_html');
if (!iframe || !iframe.contentWindow) {
alert('Ramka z fakturą nie została znaleziona.');
return false;
}
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const nabywcaDiv = iframeDoc.querySelector('.i_nabywca.col-4');
if (!nabywcaDiv) {
alert('Sekcja nabywcy nie została znaleziona w fakturze.');
return false;
}
// Check if EORI is already added
if (iframeDoc.querySelector('.eori-number')) {
alert('Numer EORI został już dodany.');
return false;
}
const br = iframeDoc.createElement('br');
const eoriTextNode = iframeDoc.createTextNode('EORI Number: ');
const eoriSpan = iframeDoc.createElement('span');
eoriSpan.className = 'eori-number';
eoriSpan.innerText = 'GB174521806000';
nabywcaDiv.appendChild(br);
nabywcaDiv.appendChild(eoriTextNode);
nabywcaDiv.appendChild(eoriSpan);
return false;
};
eoriButtonGroup.appendChild(eoriButton);
// Create "Wydrukuj z EORI" button
const printEoriButtonGroup = document.createElement('div');
printEoriButtonGroup.className = 'btn-group flex-grow-1 mb-1';
const printEoriButton = document.createElement('a');
printEoriButton.className = 'btn btn-secondary btn-block';
printEoriButton.href = '#';
printEoriButton.innerHTML = '<i class="fa fa-print"></i> Wydrukuj z EORI';
printEoriButton.onclick = function(e) {
e.preventDefault();
const iframe = document.getElementById('invoice_html');
if (iframe && iframe.contentWindow) {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const iframeWin = iframe.contentWindow;
// Inject style to force printing background colors and shadows
const style = iframeDoc.createElement('style');
style.id = 'print-fix-styles';
style.innerHTML = `
@media print {
body, body * {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
print-color-adjust: exact !important;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: #f9f9f9 !important;
}
thead, thead *, thead th, thead th * {
color: black !important;
-webkit-text-fill-color: black !important;
visibility: visible !important;
}
}
`;
iframeDoc.head.appendChild(style);
const removePrintStyles = () => {
const styleToRemove = iframeDoc.getElementById('print-fix-styles');
if (styleToRemove) {
iframeDoc.head.removeChild(styleToRemove);
}
};
const mediaQueryList = iframeWin.matchMedia('print');
const handler = (mql) => {
if (!mql.matches) {
removePrintStyles();
// Use the correct method based on browser support
try {
if (mql.removeEventListener) {
mql.removeEventListener('change', handler);
} else if (mql.removeListener) {
mql.removeListener(handler);
}
} catch (e) {
// Ignore errors when removing listeners
console.log('Error removing print listener:', e);
}
}
};
try {
if (mediaQueryList.addEventListener) {
mediaQueryList.addEventListener('change', handler);
} else if (mediaQueryList.addListener) {
mediaQueryList.addListener(handler);
}
} catch (e) {
console.log('Error adding print listener:', e);
}
iframeWin.focus();
iframeWin.print();
} else {
window.print();
}
return false;
};
printEoriButtonGroup.appendChild(printEoriButton);
// Create "Pobierz z EORI" button
const downloadEoriButtonGroup = document.createElement('div');
downloadEoriButtonGroup.className = 'btn-group flex-grow-1 mr-1 mb-1';
const downloadEoriButton = document.createElement('a');
downloadEoriButton.className = 'btn btn-secondary btn-block';
downloadEoriButton.href = '#';
downloadEoriButton.innerHTML = '<i class="fa fa-download"></i> Pobierz z EORI';
downloadEoriButton.onclick = function(e) {
e.preventDefault();
const iframe = document.getElementById('invoice_html');
if (iframe && iframe.contentWindow) {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const iframeWin = iframe.contentWindow;
// Inject style to force printing background colors and shadows
const style = iframeDoc.createElement('style');
style.id = 'download-fix-styles';
style.innerHTML = `
@media print {
body, body * {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
print-color-adjust: exact !important;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: #f9f9f9 !important;
}
thead, thead *, thead th, thead th * {
color: black !important;
-webkit-text-fill-color: black !important;
visibility: visible !important;
}
}
`;
iframeDoc.head.appendChild(style);
// Show instruction to user
alert('W oknie drukowania wybierz "Zapisz jako PDF" jako drukarkę docelową.');
// Create a function to handle PDF saving
const saveAsPdf = () => {
iframeWin.print();
// Clean up styles after a delay
setTimeout(() => {
const styleToRemove = iframeDoc.getElementById('download-fix-styles');
if (styleToRemove) {
iframeDoc.head.removeChild(styleToRemove);
}
}, 1000);
};
iframeWin.focus();
setTimeout(saveAsPdf, 500);
} else {
window.print();
}
return false;
};
downloadEoriButtonGroup.appendChild(downloadEoriButton);
// Append new buttons to the target div
targetDiv.appendChild(eoriButtonGroup);
targetDiv.appendChild(printEoriButtonGroup);
targetDiv.appendChild(downloadEoriButtonGroup);
// Mark as buttons added
targetDiv.dataset.customButtonsAdded = 'true';
});
}
// Use MutationObserver to detect when new content is added to the page
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
addButtons();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();

View file

@ -0,0 +1,981 @@
// ==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 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ść (...)
* 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 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 = '<i class="fa fa-filter"></i> 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 = '<option value="">Wszyscy dostawcy</option>';
// 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<Element>}
*/
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();
}
})();

583
idea/idea-kfz-zd.user.js Normal file
View file

@ -0,0 +1,583 @@
// ==UserScript==
// @name IDEA ERP KFZ
// @namespace http://tampermonkey.net/
// @version 0.16
// @description Przełączanie widoczności produktów z korektą w module KFZ IDEA ERP
// @author Adam
// @match https://emma.ideaerp.pl/*
// @icon https://emma.ideaerp.pl/web/static/src/img/favicon.ico
// @downloadURL https://n8n.emma.net.pl/webhook/kfz-zd
// @updateURL https://n8n.emma.net.pl/webhook/kfz-zd
// @grant none
// @run-at document-end
// ==/UserScript==
try {
console.log('[Tampermonkey KFZ] Skrypt załadowany - wersja 0.16');
} catch(e) {
alert('Błąd w Tampermonkey KFZ: ' + e);
}
/* ========================OPIS==============================
WERSJA 0.16 - Znajdowanie kolumny po indeksie (data-name tylko w th, nie w td)
WYMAGANE PARAMETRY URL:
- action=276 (ID akcji KFZ - Korekty zakupu)
- menu_id=141 (ID menu KFZ)
- view_type=form (tylko widok formularza)
- Skrypt działa TYLKO na stronie faktury korygującej (KFZ)
WYMAGANE ELEMENTY:
- Element .o_notebook ul.nav.nav-tabs (zakładki na stronie)
- Tabela .o_field_x2many[name="invoice_line_ids"] table.o_list_table
- Kolumna data-name="price_subtotal" (wartość korekty)
- Element .o_pager .o_pager_value (zakres paginacji)
- Element .o_pager .o_pager_limit (całkowita liczba produktów)
DZIAŁANIE SKRYPTU:
- Dodaje przycisk w zakładkach strony
- "Pokaż korygowane produkty" ładuje wszystkie produkty i ukrywa wiersze z wartością = 0
- "Pokaż wszystko" pokazuje wszystkie wiersze i resetuje do domyślnego widoku
- Filtrowanie po kolumnie price_subtotal (nie używa indeksów kolumn)
- Bezpieczne obsługiwanie dla < 41 produktów (brak paginacji)
FUNKCJE:
- runScriptCheck() - sprawdza czy hash się zmienił i czy to właściwa strona, dodaje przycisk tylko raz
- isCorrectPage() - sprawdza czy to właściwa strona KFZ (action=276, menu_id=141, view_type=form)
- createTestButton() - tworzy przycisk w interfejsie
- parsePolishNumber() - parsuje polskie wartości numeryczne (przecinek, , etc.)
- toggleValueRows() - filtruje wiersze według wartości (ukrywa/pokazuje wartość = 0)
- changePaginationRange() - pomocnicza funkcja do zmiany zakresu paginacji
- loadAllProducts() - ładuje wszystkie produkty
- resetToDefault() - resetuje do domyślnego widoku (1-40)
- init() - inicjalizuje skrypt i czeka na poprawne parametry URL w SPA (do 5 sek)
- startElementCheck() - sprawdza elementy DOM po potwierdzeniu poprawności URL
NASTĘPNE KROKI:
- Zapamiętywanie stanu filtra między sesjami
- Dodanie wskaźnika liczby widocznych/ukrytych wierszy
AUTOMATYCZNE AKTUALIZACJE:
- Serwer aktualizacji: https://n8n.emma.net.pl/webhook/kfz-zd
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
CHANGELOG:
v0.16 (2025) - ZNAJDOWANIE KOLUMNY PO INDEKSIE Z NAGŁÓWKA
- Naprawiono problem z brakiem atrybutu data-name w komórkach <td>
- Teraz znajdujemy kolumnę po indeksie z nagłówka <th[data-name="price_subtotal"]>
- Pobieramy indeks kolumny z nagłówka i używamy go do znalezienia komórki w wierszu
- Dodano obsługę wielu wierszy w thead (pomijamy .i7_list_search_wrap)
- Dodano szczegółowe logi pokazujące liczbę znalezionych komórek w wierszu
- Podwyższenie numeru wersji do 0.16
v0.15 (2025) - POPRAWIONO PARSOWANIE WARTOŚCI Z &NBSP; I PRZECINKAMI
- Naprawiono funkcję parsePolishNumber() - zamiana WSZYSTKICH przecinków na kropki
- Dodano obsługę &amp;nbsp; (zakodowany entity) w atrybucie title
- Dodano szczegółowe logi parsowania każdej wartości
- Naprawiono problem z filtrowaniem wierszy z wartościami ujemnymi (np. -10,15)
- Filtrowanie teraz poprawnie rozpoznaje wartości z przecinkami i spacjami
- Podwyższenie numeru wersji do 0.16
v0.14 (2025) - DODANO NASŁUCHIWANIE NA ZMIANY HASH
- Dodano window.addEventListener('hashchange') do nasłuchiwania zmian URL
- Dodano funkcję runScriptCheck() która sprawdza czy dodano przycisk
- Dodano zmienną buttonAdded żeby nie dodawać przycisku wielokrotnie
- Gdy hash się zmienia, resetujemy stan i ponownie sprawdzamy
- Naprawiono problem z brakiem działania przy nawigacji z listingu
- Podwyższenie numeru wersji do 0.16
v0.13 (2025) - ZMIANA @run-at NA document-end
- Zmieniono @run-at z document-start na document-end
- Dodano try-catch blok dla wychwycenia błędów
- document-end jest bardziej kompatybilny z SPA aplikacjami
- Naprawiono problem z brakiem uruchomienia skryptu
- Podwyższenie numeru wersji do 0.16
v0.12 (2025) - ELASTYCZNE SPRAWDZANIE PARAMETRÓW URL
- Funkcja isCorrectPage() teraz rozróżnia brak view_type od błędnego view_type
- Gdy action=276 i menu_id=141 OK, ale view_type jest null - skrypt czeka
- Dodano specjalny log "⏳ Czekam na view_type=form..." gdy parametry częściowo poprawne
- To pozwala na dłuższe czekanie view_type zostanie ustawiony asynchronicznie
- Podwyższenie numeru wersji do 0.12
v0.11 (2025) - ZWIĘKSZONO CZAS OCZEKIWANIA NA URL
- Zwiększono czas oczekiwania z 5 do 10 sekund (100 prób)
- Dodano logowanie co 10 prób (mniej spam w konsoli)
- Naprawiono problem z brakiem działania przy nawigacji z listingu
- Skrypt teraz czeka dłużej na asynchroniczne ustawienie parametrów URL
- Dodano więcej szczegółowych logów przy timeout
- Podwyższenie numeru wersji do 0.11
v0.10 (2025) - @run-at document-start + natychmiastowy console.log
- Zmieniono @run-at z document-end na document-start dla wcześniejszego uruchomienia
- Dodano natychmiastowy console.log przed IIFE dla debugowania
- Naprawiono problem z brakiem logów w konsoli
- Skrypt uruchamia się teraz na samym początku ładowania strony
- Podwyższenie numeru wersji do 0.10
v0.9 (2025) - OBSŁUGA ASYNCHRONICZNEGO URL W SPA
- Skrypt czeka do 5 sekund na ustawienie prawidłowych parametrów URL
- Naprawiono problem z brakiem przycisku gdy przechodzi się z listingu
- Dodano rozszerzone logi sprawdzające parametry URL
- Funkcja startElementCheck() - osobna funkcja do sprawdzania elementów DOM
- Skrypt działa zarówno przy bezpośrednim linku jak i przy nawigacji z listingu
- URLSearchParams parsuje parametry niezależnie od kolejności
- Podwyższenie numeru wersji do 0.9
v0.8 (2025) - DODANO KONTROLĘ URL
- Dodano funkcję isCorrectPage() sprawdzającą parametry URL
- Skrypt działa tylko na stronie KFZ (action=276, menu_id=141, view_type=form)
- Poprawiono parametry z action=278 na action=276
- Zmieniono menu_id=167 lub 141 na tylko menu_id=141
- Dodano sprawdzanie view_type=form (tylko widok formularza)
- Skrypt nie uruchamia się na niewłaściwych stronach
- Podwyższenie numeru wersji do 0.8
v0.7 (2025) - DODANO FILTROWANIE WIERSZY WEDŁUG WARTOŚCI
- Dodano funkcję toggleValueRows() do ukrywania wierszy z wartością = 0
- Dodano funkcję parsePolishNumber() do parsowania polskich wartości
- Filtrowanie po kolumnie data-name="price_subtotal" (nie używa indeksów)
- "Pokaż korygowane produkty" ukrywa wiersze z wartością = 0
- "Pokaż wszystko" pokazuje wszystkie wiersze
- Dodano logi pokazujące wartość każdego wiersza
- Podwyższenie numeru wersji do 0.7
v0.6 (2025) - POPRAWKA SPRAWDZANIA PUSTYCH WARTOŚCI PAGINACJI
- Dodano sprawdzenie czy totalProducts i currentRange nie puste
- Eliminacja problemu z ustawianiem zakresu "1-" (pusta wartość)
- Teraz poprawnie wykrywa gdy wartości puste i przerywa działanie
- Podwyższenie numeru wersji do 0.6
v0.5 (2025) - POPRAWKA OBSŁUGI < 41 PRODUKTÓW
- Dodano lepsze zabezpieczenia w loadAllProducts() dla < 41 produktów
- Dodano sprawdzenie w resetToDefault() - nie resetuje gdy brak paginacji
- Eliminacja problemu z pojawianiem się paginacji po pierwszym kliknięciu
- Dodano szczegółowe logi do debugowania
- Podwyższenie numeru wersji do 0.5
v0.4 (2025) - POPRAWKA LOGIKI PAGINACJI I BEZPIECZEŃSTWA
- Dodano funkcję changePaginationRange() - wspólna logika zmiany zakresu
- Dodano funkcję resetToDefault() - reset do domyślnego widoku
- Poprawiono logikę przycisku: "Pokaż korygowane" wszystkie produkty, "Pokaż wszystko" 1-40
- Bezpieczne obsługiwanie < 41 produktów (brak paginacji nie powoduje błędów)
- Eliminacja problemu z pokazywaniem tylko ostatniego produktu
- Podwyższenie numeru wersji do 0.4
v0.3 (2025) - POPRAWKA ZAKRESU PAGINACJI
- Poprawiono zakres paginacji z "195" na "1-195" (pełny zakres)
- Teraz pokazuje wszystkie produkty zamiast tylko ostatniego
- Zaktualizowano logi konsoli dla lepszej czytelności
- Podwyższenie numeru wersji do 0.3
v0.2 (2025) - ŁADOWANIE WSZYSTKICH PRODUKTÓW
- Dodano funkcję loadAllProducts() do automatycznego ładowania wszystkich produktów
- Po kliknięciu przycisku zmienia zakres paginacji z "1-40" na "1-{całkowita_liczba}"
- Implementacja poprzez kliknięcie w .o_pager_value, wpisanie wartości i naciśnięcie Enter
- Podwyższenie numeru wersji do 0.2
v0.1 (2025) - TESTOWA WERSJA
- Dodano podstawowy przycisk w zakładkach
- Przycisk zmienia swoją nazwę po kliknięciu
- Brak funkcjonalności filtrowania
- Podwyższenie numeru wersji do 0.1 (testowa wersja)
============================================================ */
(() => {
'use strict';
console.log('[Tampermonkey KFZ] Wersja 0.16 - znajdowanie kolumny po indeksie z nagłówka');
// Zmienne do śledzenia stanu
let lastHash = '';
let buttonAdded = false;
// Funkcja do uruchomienia skryptu
function runScriptCheck() {
console.log('[Tampermonkey KFZ] runScriptCheck wywołane, hash:', window.location.hash);
// Jeśli już dodaliśmy przycisk, nie dodajemy ponownie
if (buttonAdded) {
console.log('[Tampermonkey KFZ] Przycisk już dodany, pomijam');
return;
}
// Jeśli hash się nie zmienił, nie sprawdzamy ponownie
if (lastHash === window.location.hash) {
console.log('[Tampermonkey KFZ] Hash bez zmian, pomijam');
return;
}
lastHash = window.location.hash;
// Sprawdzamy czy to właściwa strona
if (isCorrectPage()) {
console.log('[Tampermonkey KFZ] ✅ Poprawna strona, uruchamiam startElementCheck()');
buttonAdded = true; // Oznaczamy że dodaliśmy przycisk
startElementCheck();
}
}
// Funkcja sprawdzająca czy to właściwa strona (KFZ - korekty zakupu)
function isCorrectPage() {
const hash = window.location.hash;
console.log('[Tampermonkey KFZ] Sprawdzam URL hash:', hash);
console.log('[Tampermonkey KFZ] Pełny URL:', window.location.href);
// Parametry są w hash, np. #action=276&menu_id=141&view_type=form
if (!hash || hash.length <= 1) {
console.log('[Tampermonkey KFZ] Brak parametrów w hash lub hash jest pusty');
return false;
}
if (!hash.includes('action=')) {
console.log('[Tampermonkey KFZ] Hash nie zawiera parametru action');
return false;
}
// Parsujemy parametry z hash
const params = new URLSearchParams(hash.substring(1)); // usuwamy #
const action = params.get('action');
const menuId = params.get('menu_id');
const viewType = params.get('view_type');
console.log('[Tampermonkey KFZ] Parsowanie: action =', action, ', menu_id =', menuId, ', view_type =', viewType);
// Sprawdzamy czy to strona korekty zakupu
// Wymagamy action=276 i menu_id=141
// view_type może być jeszcze nie ustawiony (będzie ustawiony później)
const hasCorrectAction = action === '276';
const hasCorrectMenu = menuId === '141';
const hasFormView = viewType === 'form';
const isWaitingForViewType = hasCorrectAction && hasCorrectMenu && !viewType;
const isCorrect = hasCorrectAction && hasCorrectMenu && hasFormView;
if (isWaitingForViewType) {
console.log('[Tampermonkey KFZ] ⏳ Czekam na view_type=form...');
console.log('[Tampermonkey KFZ] Otrzymano: action=' + action + ', menu_id=' + menuId + ', view_type=' + viewType);
return false; // Jeszcze czekamy
}
if (!isCorrect) {
console.log('[Tampermonkey KFZ] ❌ To nie jest strona KFZ (korekty zakupu na formularzu)');
console.log('[Tampermonkey KFZ] Wymagane: action=276, menu_id=141, view_type=form');
console.log('[Tampermonkey KFZ] Otrzymano: action=' + action + ', menu_id=' + menuId + ', view_type=' + viewType);
} else {
console.log('[Tampermonkey KFZ] ✅ Poprawna strona KFZ wykryta');
}
return isCorrect;
}
// Funkcja pomocnicza do zmiany zakresu paginacji
function changePaginationRange(newRange) {
console.log('[Tampermonkey KFZ] Zmieniam zakres paginacji na:', newRange);
const pager = document.querySelector('.o_field_x2many[name="invoice_line_ids"] .o_pager');
if (!pager) {
console.log('[Tampermonkey KFZ] Nie znaleziono paginacji (mniej niż 41 produktów)');
return false;
}
const pagerValue = pager.querySelector('.o_pager_value');
if (!pagerValue) {
console.log('[Tampermonkey KFZ] Nie znaleziono elementu .o_pager_value');
return false;
}
pagerValue.click();
setTimeout(() => {
const input = pager.querySelector('.o_pager_value input');
if (input) {
input.value = newRange;
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
bubbles: true
});
input.dispatchEvent(enterEvent);
console.log('[Tampermonkey KFZ] Ustawiono zakres na:', newRange);
}
}, 500);
return true;
}
// Funkcja do parsowania polskich wartości numerycznych
function parsePolishNumber(value) {
if (!value) return 0;
console.log('[Tampermonkey KFZ] Parsuję wartość:', value);
// Usuwamy "zł", wszystkie spacje (w tym &nbsp; i &amp;nbsp;), zastępujemy przecinki kropkami
let cleanValue = value
.replace(/zł/gi, '') // Usuń zł
.replace(/\s+/g, '') // Usuń wszystkie białe znaki (w tym zwykłe spacje)
.replace(/&nbsp;/g, '') // Usuń &nbsp;
.replace(/&amp;nbsp;/g, '') // Usuń zakodowane &amp;nbsp;
.replace(/,/g, '.'); // Zamień WSZYSTKIE przecinki na kropki
console.log('[Tampermonkey KFZ] Oczyszczona wartość:', cleanValue);
const result = parseFloat(cleanValue) || 0;
console.log('[Tampermonkey KFZ] Wynik:', result);
return result;
}
// Funkcja do filtrowania wierszy - ukrywanie/pokazywanie wierszy z wartością = 0
function toggleValueRows(showOnlyWithValue) {
console.log('[Tampermonkey KFZ] Filtruję wiersze - pokazuję tylko z wartością ≠ 0:', showOnlyWithValue);
// Szukamy tabeli pozycji faktury
const table = document.querySelector('.o_field_x2many[name="invoice_line_ids"] table.o_list_table');
if (!table) {
console.log('[Tampermonkey KFZ] Nie znaleziono tabeli');
return;
}
// Szukamy nagłówka kolumny "Wartość" po data-name
// W thead może być kilka wierszy, bierzemy pierwszy (nie .i7_list_search_wrap)
const headerRow = table.querySelector('thead tr:not(.i7_list_search_wrap)');
const headers = headerRow.querySelectorAll('th');
let priceSubtotalIndex = -1;
headers.forEach((th, idx) => {
if (th.getAttribute('data-name') === 'price_subtotal') {
priceSubtotalIndex = idx;
console.log('[Tampermonkey KFZ] Znaleziono kolumnę price_subtotal na indeksie', priceSubtotalIndex);
}
});
if (priceSubtotalIndex === -1) {
console.log('[Tampermonkey KFZ] Nie znaleziono kolumny "price_subtotal"');
return;
}
// Pobieramy wszystkie wiersze danych
const rows = table.querySelectorAll('tbody tr.o_data_row');
console.log('[Tampermonkey KFZ] Znaleziono', rows.length, 'wierszy');
let hiddenCount = 0;
let visibleCount = 0;
// Przetwarzamy każdy wiersz
rows.forEach((row, index) => {
// Szukamy wszystkich komórek w wierszu (włączając th i td)
const cells = row.querySelectorAll('th, td');
console.log('[Tampermonkey KFZ] Wiersz', index + 1, '- znaleziono', cells.length, 'komórek');
// Pobieramy komórkę po indeksie kolumny
// UWAGA: W wierszu pierwsza komórka to <th>, więc indeks się zgadza
const valueCell = cells[priceSubtotalIndex];
if (!valueCell) {
console.log('[Tampermonkey KFZ] Wiersz', index + 1, '- brak komórki na indeksie', priceSubtotalIndex);
return;
}
console.log('[Tampermonkey KFZ] Wiersz', index + 1, '- komórka:', valueCell);
// Pobieramy wartość z atrybutu title lub textContent
const valueText = valueCell.getAttribute('title') || valueCell.textContent;
const numericValue = parsePolishNumber(valueText);
console.log('[Tampermonkey KFZ] Wiersz', index + 1, ':', valueText, '->', numericValue);
if (showOnlyWithValue) {
// Pokaż tylko wiersze z wartością ≠ 0
if (numericValue !== 0) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
hiddenCount++;
}
} else {
// Pokaż wszystkie wiersze
row.style.display = '';
visibleCount++;
}
});
console.log('[Tampermonkey KFZ] Filtrowanie zakończone - widoczne:', visibleCount, 'ukryte:', hiddenCount);
}
// Funkcja do ładowania wszystkich produktów
function loadAllProducts() {
console.log('[Tampermonkey KFZ] Ładuję wszystkie produkty...');
// Szukamy elementów paginacji - trzeba znaleźć je w kontekście tabeli pozycji faktury
const pager = document.querySelector('.o_field_x2many[name="invoice_line_ids"] .o_pager');
// Jeśli nie ma paginacji, to znaczy że jest mniej niż 41 produktów - wszystkie są już widoczne
if (!pager) {
console.log('[Tampermonkey KFZ] Nie ma paginacji - wszystkie produkty (< 41) są już widoczne');
return;
}
const pagerLimit = pager.querySelector('.o_pager_limit');
const pagerValue = pager.querySelector('.o_pager_value');
if (!pagerLimit || !pagerValue) {
console.log('[Tampermonkey KFZ] Nie znaleziono elementów paginacji');
return;
}
const totalProducts = pagerLimit.textContent.trim();
const currentRange = pagerValue.textContent.trim();
console.log('[Tampermonkey KFZ] Aktualny zakres:', currentRange, 'Całkowita liczba:', totalProducts);
// Jeśli wartości są puste, to znaczy że nie ma prawidłowej paginacji
if (!totalProducts || !currentRange) {
console.log('[Tampermonkey KFZ] Wartości paginacji są puste - wszystkie produkty są już widoczne');
return;
}
// Sprawdzamy, czy już są pokazane wszystkie produkty
if (currentRange.includes(`1-${totalProducts}`)) {
console.log('[Tampermonkey KFZ] Wszystkie produkty są już załadowane');
return;
}
console.log('[Tampermonkey KFZ] Zmieniam zakres z', currentRange, 'na 1-', totalProducts);
changePaginationRange(`1-${totalProducts}`);
}
// Funkcja do resetowania do domyślnego widoku
function resetToDefault() {
console.log('[Tampermonkey KFZ] Resetuję do domyślnego widoku...');
// Sprawdzamy czy w ogóle jest paginacja
const pager = document.querySelector('.o_field_x2many[name="invoice_line_ids"] .o_pager');
if (!pager) {
console.log('[Tampermonkey KFZ] Nie ma paginacji (< 41 produktów) - reset nie jest potrzebny');
return;
}
changePaginationRange('1-40');
}
function createTestButton() {
// Szukamy przycisku w zakładkach
const nav = document.querySelector('.o_notebook ul.nav.nav-tabs');
if (!nav) {
console.log('[Tampermonkey KFZ] Nie znaleziono nav-tabs');
return;
}
console.log('[Tampermonkey KFZ] Znaleziono nav-tabs, tworzę przycisk');
// Tworzymy element li
const li = document.createElement('li');
li.className = 'nav-item';
// Tworzymy link
const a = document.createElement('a');
a.href = '#';
a.className = 'nav-link';
a.textContent = 'Pokaż korygowane produkty';
// Dodajemy event listener
a.addEventListener('click', async (e) => {
e.preventDefault();
// Sprawdzamy aktualną nazwę
if (a.textContent === 'Pokaż korygowane produkty') {
a.textContent = 'Pokaż wszystko';
console.log('[Tampermonkey KFZ] Zmieniono nazwę na: Pokaż wszystko');
// Ładujemy wszystkie produkty
loadAllProducts();
// Po załadowaniu filtrujemy wiersze (ukrywamy wartość = 0)
setTimeout(() => {
toggleValueRows(true);
}, 1000);
} else {
a.textContent = 'Pokaż korygowane produkty';
console.log('[Tampermonkey KFZ] Zmieniono nazwę na: Pokaż korygowane produkty');
// Pokaż wszystkie wiersze
toggleValueRows(false);
// Resetujemy do domyślnego widoku
resetToDefault();
}
});
li.appendChild(a);
nav.appendChild(li);
console.log('[Tampermonkey KFZ] Przycisk dodany pomyślnie');
}
// Funkcja inicjalizująca
function init() {
console.log('[Tampermonkey KFZ] Inicjalizacja...');
// W aplikacji SPA parametry URL mogą być ustawiane asynchronicznie
// Czekamy do 10 sekund na poprawne parametry URL
let attempts = 0;
const maxAttempts = 100; // 100 * 100ms = 10 sekund
console.log('[Tampermonkey KFZ] Rozpoczynam sprawdzanie URL (max 100 prób co 100ms)');
// Nasłuchujemy na zmiany URL (hashchange)
window.addEventListener('hashchange', () => {
console.log('[Tampermonkey KFZ] 🎯 Hash changed!');
buttonAdded = false; // Resetujemy stan - możliwa nowa strona
lastHash = ''; // Resetujemy hash żeby wykryć zmianę
setTimeout(runScriptCheck, 200);
});
const urlCheckInterval = setInterval(() => {
attempts++;
// Log co 10 prób
if (attempts % 10 === 0 || attempts === 1) {
console.log('[Tampermonkey KFZ] Próba', attempts, '/', maxAttempts, 'sprawdzenia URL...');
}
runScriptCheck();
if (attempts >= maxAttempts) {
console.log('[Tampermonkey KFZ] ❌ Timeout po', attempts, 'próbach');
clearInterval(urlCheckInterval);
}
}, 100); // Sprawdzamy co 100ms
}
// Funkcja sprawdzająca elementy strony po potwierdzeniu URL
function startElementCheck() {
// Szukamy elementów strony
const checkInterval = setInterval(() => {
const nav = document.querySelector('.o_notebook ul.nav.nav-tabs');
console.log('[Tampermonkey KFZ] Sprawdzam nav-tabs:', !!nav);
if (nav) {
clearInterval(checkInterval);
createTestButton();
}
}, 500);
// Timeout po 10 sekundach
setTimeout(() => {
clearInterval(checkInterval);
console.log('[Tampermonkey KFZ] Timeout - nie znaleziono nav-tabs');
}, 10000);
}
// Start
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

784
idea/idea-kfz.user.js Normal file
View file

@ -0,0 +1,784 @@
// ==UserScript==
// @name IDEAERP - KFZ Korekty
// @namespace http://tampermonkey.net/
// @version 1.8
// @description Kopiowanie danych z tabeli KFZ, pól formularza i podsumowania - kliknij aby skopiować
// @match https://emma.ideaerp.pl/web*
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
// @downloadURL https://n8n.emma.net.pl/webhook/kfz
// @updateURL https://n8n.emma.net.pl/webhook/kfz
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @run-at document-end
// ==/UserScript==
/* ========================OPIS==============================
WERSJA 1.1 - Funkcjonalność kopiowania danych z tabeli KFZ, pól formularza i podsumowania
WYMAGANE PARAMETRY URL:
- action=278 ID akcji KFZ
- menu_id=141 ID menu KFZ
- model=account.move Model faktury/dokumentu
- view_type=form Widok formularza
WYMAGANE KOLUMNY w tabeli KFZ:
- product_id Produkt
- quantity_after Ilość po korekcie
- price_unit_after Cena jednostkowa po korekcie
- price_subtotal Wartość
- invoice_line_net Netto
- invoice_line_tax Podatek
- invoice_line_gross Brutto
DZIAŁANIE SKRYPTU:
- Kopiowanie produktów z trybem SKU/pełna nazwa (jak w idea-orders)
- Kopiowanie wartości liczbowych z kolumn KFZ
- Ikona kopiowania przy każdej kopiowalnej komórce
- Przycisk przełączania trybu kopiowania produktów
- Obsługa błędów zgodnie z zasadami .rules
- Automatyczne wykrywanie kolumn po atrybucie title
FUNKCJE KOPIOWANIA:
- Produkt: SKU (z nawiasów kwadratowych) lub pełna nazwa
- Ilość po korekcie: wartość liczbowa
- Cena jednostkowa po korekcie: wartość liczbowa
- Wartość: wartość liczbowa
- Netto: wartość liczbowa
- Podatek: wartość liczbowa
- Brutto: wartość liczbowa
ZAPAMIĘTYWANIE USTAWIEŃ:
- Tryb kopiowania produktów (SKU/pełna nazwa) zapisywany w pamięci
- Ustawienia przetrwają restart przeglądarki i odświeżenie strony
AUTOMATYCZNE AKTUALIZACJE:
- Serwer aktualizacji: https://n8n.emma.net.pl/webhook/kfz
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
CHANGELOG:
v1.8 (2024) - PRZYWRÓCONO DWUKROPEK W TEKŚCIE PRZYCISKU
- Przywrócono dwukropek po słowie "Kopiuję"
- Tekst przycisku: "Kopiuję: SKU" i "Kopiuję: Pełna nazwa"
- Podwyższenie numeru wersji do 1.8
v1.7 (2024) - USUNIĘTO DWUKROPEK Z TEKSTU PRZYCISKU
- Zmieniono tekst przycisku z "Kopiuję: SKU" na "Kopiuję SKU"
- Zmieniono tekst z "Kopiuję: Pełna nazwa" na "Kopiuję Pełna nazwa"
- Usunięto dwukropek dla lepszej czytelności
- Podwyższenie numeru wersji do 1.7
v1.6 (2024) - POPRAWKA LOKALIZACJI PRZYCISKU W TAB-CONTENT
- Poprawiono findActionsContainer() - szuka w .tab-content > .tab-pane
- Przycisk umieszczany w zakładce obok "Załaduj z cennika" i "Masowa zmiana"
- Dodano wyszukiwanie zakładek z przyciskami (load_from_pricelist, button[name="523"])
- Przycisk w właściwym miejscu: <div class="tab-content"> zamiast o_control_panel
- Podwyższenie numeru wersji do 1.6
v1.5 (2024) - POPRAWKA UMIESZCZANIA PRZYCISKU
- Poprawiono funkcję findActionsContainer() - priorytet dla .o_form_buttons_view
- Przycisk "Kopiuję: SKU" teraz umieszczany w tym samym miejscu co w idea-orders
- Usunięto skomplikowaną logikę umieszczania w zakładkach
- Dokładnie ta sama strategia umieszczania co w idea-orders.user.js
- Podwyższenie numeru wersji do 1.5
v1.4 (2024) - DODANO BRAKUJĄCE POLA
- Dodano kopiowanie nazwy dokumentu (name) - np. "KFZ/302/10/2025"
- Dodano kopiowanie NIP (invoice_name) - np. "CSG SPÓŁKA AKCYJNA"
- Poprawiono mapowanie pól formularza na rzeczywiste selektory HTML
- Rozszerzone selektory dla pola name (również h1 span[name="name"])
- Podwyższenie numeru wersji do 1.4
v1.3 (2024) - NAPRAWKA BŁĘDU SELEKTORÓW CSS
- Usunięto nieprawidłowe selektory :contains() powodujące błąd SyntaxError
- Dodano bezpieczne try/catch dla wszystkich selektorów
- Alternatywne szukanie pól po etykietach bez :contains()
- Naprawiono blokowanie strony przez nieprawidłowe selektory
- Podwyższenie numeru wersji do 1.3
v1.2 (2024) - POPRAWKI KOPIOWANIA VAT I PÓL FORMULARZA
- Poprawiono kopiowanie VAT - znaczek kopiowania przy wartościach, nie nazwach
- Dodano commercial_partner_id jako "Nazwa na fakturę"
- Rozszerzone selektory dla NIP (partner_vat, vat)
- Dodano kopiowanie uwag (note, narration)
- Lepsze wykrywanie elementów po etykietach
- Podwyższenie numeru wersji do 1.2
v1.1 (2024) - KOPIOWANIE PÓL FORMULARZA I PODSUMOWANIA
- Dodano kopiowanie pól formularza: Nazwa na fakturę, NIP, Odnosnik, Data faktury
- Dodano kopiowanie pól podsumowania: Należność, Suma, Kwota bez podatku
- Dodano kopiowanie VAT (różne stawki)
- Automatyczna konwersja wartości ujemnych na dodatnie w podsumowaniu
- Funkcja makePositive() dla zapewnienia dodatnich wartości
- Rozszerzone selektory dla różnych typów elementów (span, a, input)
- Podwyższenie numeru wersji do 1.1
v1.0 (2024) - PIERWSZA WERSJA
- Dodano kopiowanie produktów z trybem SKU/pełna nazwa
- Dodano kopiowanie wartości liczbowych z 6 kolumn KFZ
- Dodano style CSS dla elementów kopiowalnych (pomarańczowy kolor #ff9800)
- Dodano przycisk przełączania trybu kopiowania
- Dodano menu Tampermonkey z opcjami konfiguracji
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
- Automatyczne wykrywanie kolumn po title zamiast indeksów
- Spójny styl wizualny z idea-orders.user.js
- Precyzyjne wykrywanie strony KFZ po parametrach action=278 i menu_id=141
============================================================ */
(function () {
'use strict';
// Debug flag zgodnie z zasadami .rules
const DEBUG = false;
const log = (...args) => DEBUG && console.log('[Tampermonkey KFZ]', ...args);
// ——— Klucze stanu ———
const COPY_MODE_KEY = 'tm_kfz_product_copy_mode_v10'; // true = SKU, false = pełna nazwa (domyślnie true)
const TABLE_SEL = 'table.o_list_table.o_section_and_note_list_view';
// Mapowanie title na data-name dla kolumn do kopiowania
const COPYABLE_COLUMNS = {
'Produkt': 'product_id',
'Ilość po korekcie': 'quantity_after',
'Cena jednostkowa po korekcie': 'price_unit_after',
'Wartość': 'price_subtotal',
'Netto': 'invoice_line_net',
'Podatek': 'invoice_line_tax',
'Brutto': 'invoice_line_gross'
};
// Pola formularza do kopiowania
const COPYABLE_FORM_FIELDS = {
'name': 'Nazwa dokumentu',
'commercial_partner_id': 'Nazwa na fakturę',
'partner_id': 'Kontrahent',
'invoice_name': 'NIP',
'partner_vat': 'NIP (alternatywny)',
'ref': 'Odnosnik',
'invoice_date': 'Data faktury',
'note': 'Uwagi',
'narration': 'Uwagi wewnętrzne'
};
// Pola podsumowania do kopiowania
const COPYABLE_SUMMARY_FIELDS = {
'amount_residual': 'Należność',
'amount_total': 'Suma',
'amount_untaxed': 'Kwota bez podatku'
};
GM_addStyle(`
/* 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));
}
`);
// ——— Pomocnicze funkcje ———
const textOf = el => el ? (el.getAttribute('title') || el.textContent || '').trim() : '';
// Funkcja do konwersji wartości na dodatnią
const makePositive = (value) => {
if (typeof value === 'string') {
// Usuń znaki waluty i spacje
const cleanValue = value.replace(/[^\d,.-]/g, '').replace(',', '.');
const numValue = parseFloat(cleanValue);
return isNaN(numValue) ? value : Math.abs(numValue).toString();
}
return Math.abs(parseFloat(value) || 0).toString();
};
function isKFZForm() {
const params = new URLSearchParams((location.hash || '').replace(/^#/, ''));
const action = params.get('action');
const menuId = params.get('menu_id');
const model = params.get('model');
const viewType = params.get('view_type');
log('Sprawdzanie parametrów URL:', {
action, menuId, model, viewType,
hash: location.hash,
url: location.href
});
// Sprawdź czy to właściwa strona KFZ z konkretnymi parametrami
const isKFZ = action === '278' &&
menuId === '141' &&
model === 'account.move' &&
viewType === 'form';
log('Czy to strona KFZ?', isKFZ);
return isKFZ;
}
// ——— Funkcja wyodrębniania SKU z nawiasów kwadratowych ———
function extractSKU(productText) {
const match = productText.match(/\[([^\]]+)\]/);
return match ? match[1] : productText;
}
// ——— Schowek: kopiowanie wartości z komórek ———
async function tmCopyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch (error) {
log('Fallback do document.execCommand dla kopiowania');
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
// ——— Kopiowanie zwykłych komórek (wartości liczbowe) ———
function makeCellCopyable(td, makePositiveValue = false) {
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;
// Konwertuj na wartość dodatnią jeśli wymagane
if (makePositiveValue) {
val = makePositive(val);
}
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 komórkę:', val);
} catch (error) {
console.error('[Tampermonkey KFZ] Błąd podczas kopiowania komórki:', error);
td.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Kopiowanie produktów z trybem SKU/pełna nazwa ———
function makeProductCellCopyable(td) {
if (!td || td.dataset.tmProductCopyable) return;
td.dataset.tmProductCopyable = '1';
td.classList.add('tm-copyable');
td.addEventListener('click', async (e) => {
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
const fullText = (td.textContent || '').trim();
if (!fullText) return;
try {
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true); // domyślnie SKU
const textToCopy = copyOnlySKU ? extractSKU(fullText) : fullText;
await tmCopyToClipboard(textToCopy);
const old = td.title;
const mode = copyOnlySKU ? 'SKU' : 'pełną nazwę';
td.title = `Skopiowano ${mode}: ${textToCopy}`;
td.classList.add('tm-copied');
setTimeout(() => {
td.title = old || 'Skopiowano';
td.classList.remove('tm-copied');
}, 1200);
log('Skopiowano produkt:', textToCopy, 'tryb:', mode);
} catch (error) {
console.error('[Tampermonkey KFZ] Błąd podczas kopiowania produktu:', error);
td.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Kopiowanie pól formularza ———
function makeFormFieldCopyable(element, fieldName) {
if (!element || element.dataset.tmFormCopyable) return;
element.dataset.tmFormCopyable = '1';
element.classList.add('tm-copyable');
element.style.cursor = 'copy';
element.title = `Kliknij aby skopiować ${fieldName}`;
element.addEventListener('click', async (e) => {
e.stopPropagation();
let text = '';
// Różne sposoby wyciągania tekstu w zależności od typu elementu
if (element.tagName === 'SPAN') {
text = (element.textContent || '').trim();
} else if (element.tagName === 'A') {
text = (element.querySelector('span')?.textContent || element.textContent || '').trim();
} else {
text = (element.textContent || element.value || '').trim();
}
if (!text) return;
try {
await tmCopyToClipboard(text);
const old = element.title;
element.title = `Skopiowano ${fieldName}: ${text}`;
element.classList.add('tm-copied');
setTimeout(() => {
element.title = old || `Kliknij aby skopiować ${fieldName}`;
element.classList.remove('tm-copied');
}, 1200);
log(`Skopiowano ${fieldName}:`, text);
} catch (error) {
console.error(`[Tampermonkey KFZ] Błąd podczas kopiowania ${fieldName}:`, error);
element.title = 'Błąd podczas kopiowania';
}
}, true);
}
// ——— Kopiowanie pól podsumowania (z konwersją na wartości dodatnie) ———
function makeSummaryFieldCopyable(element, fieldName) {
if (!element || element.dataset.tmSummaryCopyable) return;
element.dataset.tmSummaryCopyable = '1';
element.classList.add('tm-copyable');
element.style.cursor = 'copy';
element.title = `Kliknij aby skopiować ${fieldName}`;
element.addEventListener('click', async (e) => {
e.stopPropagation();
let text = (element.textContent || '').trim();
if (!text) return;
// Konwertuj na wartość dodatnią
const positiveValue = makePositive(text);
try {
await tmCopyToClipboard(positiveValue);
const old = element.title;
element.title = `Skopiowano ${fieldName}: ${positiveValue}`;
element.classList.add('tm-copied');
setTimeout(() => {
element.title = old || `Kliknij aby skopiować ${fieldName}`;
element.classList.remove('tm-copied');
}, 1200);
log(`Skopiowano ${fieldName}:`, positiveValue);
} catch (error) {
console.error(`[Tampermonkey KFZ] Błąd podczas kopiowania ${fieldName}:`, error);
element.title = 'Błąd podczas kopiowania';
}
}, true);
}
// ——— Znajdowanie kolumn po title ———
function findColumnByTitle(table, title) {
const headers = table.querySelectorAll('thead th');
log(`Szukanie kolumny "${title}" w ${headers.length} nagłówkach`);
for (let i = 0; i < headers.length; i++) {
const headerTitle = headers[i].getAttribute('title');
const headerText = headers[i].textContent?.trim();
log(`Nagłówek ${i}: title="${headerTitle}", text="${headerText}"`);
if (headerTitle === title) {
log(`Znaleziono kolumnę "${title}" na pozycji ${i}`);
return i;
}
}
log(`Nie znaleziono kolumny "${title}"`);
return -1;
}
// ——— Główna funkcja dodawania kopiowania ———
function addCopyFunctionality(table) {
log('Dodawanie funkcjonalności kopiowania do tabeli');
log('Selektor tabeli:', table.className, table.tagName);
// Iteruj przez wszystkie wiersze danych
const rows = table.querySelectorAll('tbody tr.o_data_row');
log(`Znaleziono ${rows.length} wierszy danych`);
if (rows.length === 0) {
// Spróbuj alternatywnych selektorów
const altRows = table.querySelectorAll('tbody tr');
log(`Alternatywnie znaleziono ${altRows.length} wierszy (wszystkie)`);
altRows.forEach((row, rowIndex) => {
log(`Wiersz ${rowIndex}:`, row.className, row.children.length, 'komórek');
});
}
rows.forEach((row, rowIndex) => {
log(`Przetwarzanie wiersza ${rowIndex} z ${row.children.length} komórkami`);
// Dla każdej kolumny do kopiowania
Object.entries(COPYABLE_COLUMNS).forEach(([title, dataName]) => {
const colIndex = findColumnByTitle(table, title);
if (colIndex === -1) {
return;
}
const cell = row.children[colIndex];
if (!cell) {
log(`Brak komórki w wierszu ${rowIndex}, kolumna ${colIndex}`);
return;
}
log(`Dodawanie kopiowania do komórki [${rowIndex}, ${colIndex}] "${title}": "${cell.textContent?.trim()}"`);
// Specjalne traktowanie dla kolumny Produkt
if (title === 'Produkt') {
makeProductCellCopyable(cell);
} else {
makeCellCopyable(cell);
}
});
});
}
// ——— Dodawanie kopiowania dla pól formularza ———
function addFormFieldsCopyFunctionality() {
log('Dodawanie kopiowania dla pól formularza...');
Object.entries(COPYABLE_FORM_FIELDS).forEach(([fieldName, displayName]) => {
// Szukaj różnych selektorów dla pól
let selectors = [
`span[name="${fieldName}"]`,
`a[name="${fieldName}"]`,
`input[name="${fieldName}"]`,
`[name="${fieldName}"] span`,
`[name="${fieldName}"]`
];
// Specjalne selektory dla konkretnych pól
if (fieldName === 'invoice_name') {
selectors = [
'span[name="invoice_name"]',
'input[name="invoice_name"]',
'[name="invoice_name"]'
];
} else if (fieldName === 'name') {
selectors = [
'span[name="name"]',
'input[name="name"]',
'[name="name"]',
'h1 span[name="name"]'
];
} else if (fieldName === 'partner_vat') {
selectors = [
'span[name="partner_vat"]',
'input[name="partner_vat"]',
'[name="partner_vat"]',
'span[name="vat"]',
'input[name="vat"]',
'[name="vat"]'
];
} else if (fieldName === 'note' || fieldName === 'narration') {
selectors = [
`span[name="${fieldName}"]`,
`textarea[name="${fieldName}"]`,
`div[name="${fieldName}"]`,
`[name="${fieldName}"]`
];
}
let found = false;
for (const selector of selectors) {
try {
const element = document.querySelector(selector);
if (element && element.textContent?.trim()) {
log(`Znaleziono pole ${displayName} (${fieldName}):`, element.tagName, element.textContent?.trim());
makeFormFieldCopyable(element, displayName);
found = true;
break;
}
} catch (error) {
log(`Błąd selektora "${selector}":`, error.message);
}
}
// Jeśli nie znaleziono standardowymi selektorami, spróbuj szukać po etykietach
if (!found) {
if (fieldName === 'partner_vat') {
const labels = document.querySelectorAll('label');
for (const label of labels) {
if (label.textContent?.includes('NIP') || label.textContent?.includes('VAT')) {
const nextElement = label.nextElementSibling?.querySelector('span') ||
label.parentElement?.nextElementSibling?.querySelector('span');
if (nextElement && nextElement.textContent?.trim()) {
log(`Znaleziono ${displayName} przez etykietę:`, nextElement.textContent?.trim());
makeFormFieldCopyable(nextElement, displayName);
break;
}
}
}
} else if (fieldName === 'note' || fieldName === 'narration') {
const labels = document.querySelectorAll('label');
for (const label of labels) {
if (label.textContent?.includes('Uwagi') || label.textContent?.includes('Notatki') || label.textContent?.includes('Opis')) {
const nextElement = label.nextElementSibling?.querySelector('span, textarea, div') ||
label.parentElement?.nextElementSibling?.querySelector('span, textarea, div');
if (nextElement && nextElement.textContent?.trim()) {
log(`Znaleziono ${displayName} przez etykietę:`, nextElement.textContent?.trim());
makeFormFieldCopyable(nextElement, displayName);
break;
}
}
}
}
}
});
}
// ——— Dodawanie kopiowania dla pól podsumowania ———
function addSummaryFieldsCopyFunctionality() {
log('Dodawanie kopiowania dla pól podsumowania...');
Object.entries(COPYABLE_SUMMARY_FIELDS).forEach(([fieldName, displayName]) => {
// Szukaj różnych selektorów dla pól podsumowania
const selectors = [
`span[name="${fieldName}"]`,
`[name="${fieldName}"]`,
`.oe_subtotal_footer_separator[name="${fieldName}"]`
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) {
log(`Znaleziono pole ${displayName} (${fieldName}):`, element.textContent?.trim());
makeSummaryFieldCopyable(element, displayName);
break;
}
}
});
// Dodatkowe szukanie VAT - znaczek kopiowania przy wartościach, nie nazwach
const vatRows = document.querySelectorAll('tr:has(.oe_tax_group_name)');
vatRows.forEach((row, index) => {
const nameElement = row.querySelector('.oe_tax_group_name');
const valueElement = row.querySelector('.oe_tax_group_amount_value, .oe_tax_group_editable span');
if (nameElement && valueElement && nameElement.textContent?.includes('VAT')) {
const vatName = nameElement.textContent?.trim() || `VAT ${index + 1}`;
log(`Znaleziono VAT "${vatName}":`, valueElement.textContent?.trim());
makeSummaryFieldCopyable(valueElement, vatName);
}
});
// Fallback - jeśli powyższe nie zadziała, szukaj bezpośrednio wartości VAT
const vatValues = document.querySelectorAll('.oe_tax_group_amount_value');
vatValues.forEach((element, index) => {
if (!element.dataset.tmSummaryCopyable) {
log(`Znaleziono wartość VAT ${index + 1}:`, element.textContent?.trim());
makeSummaryFieldCopyable(element, `VAT ${index + 1}`);
}
});
}
// ——— Lokalizowanie docelowego miejsca dla przycisku ———
function findActionsContainer() {
log('Szukanie kontenera dla przycisku...');
// PRIORYTET 1: Dokładnie jak w idea-orders.user.js - szukaj notebook_page_130
const byId = document.getElementById('notebook_page_130');
if (byId) {
log('Znaleziono notebook_page_130');
return byId;
}
// PRIORYTET 2: Szukaj przycisku load_from_pricelist i jego kontener .tab-pane
const pricelistBtn = document.querySelector('button[name="load_from_pricelist"]');
if (pricelistBtn) {
const container = pricelistBtn.closest('.tab-pane') || pricelistBtn.parentElement;
log('Znaleziono kontener przez load_from_pricelist:', container.id || container.className);
return container;
}
// PRIORYTET 3: Szukaj w .tab-content > .tab-pane (gdzie są zakładki)
const tabPanes = document.querySelectorAll('.tab-content .tab-pane');
for (const pane of tabPanes) {
// Szukaj zakładki z przyciskami (load_from_pricelist lub button[name="523"])
if (pane.querySelector('button[name="load_from_pricelist"], button[name="523"]')) {
log('Znaleziono zakładkę z przyciskami:', pane.id || pane.className);
return pane;
}
}
// PRIORYTET 4: Szukaj konkretnych zakładek KFZ - notebook_page_669 (Pozycje faktury)
const kfzTab = document.getElementById('notebook_page_669');
if (kfzTab) {
log('Znaleziono notebook_page_669 (Pozycje faktury)');
return kfzTab;
}
log('Nie znaleziono odpowiedniego kontenera');
return null;
}
// ——— Przycisk przełączania trybu kopiowania produktów ———
function addCopyModeToggleButton() {
const host = findActionsContainer();
if (!host) {
log('Nie znaleziono kontenera dla przycisku');
return;
}
const idBtn = 'tm-kfz-copy-mode-toggle-button';
if (host.querySelector('#' + idBtn)) {
log('Przycisk już istnieje');
return;
}
log('Tworzenie przycisku przełączania trybu kopiowania...');
const btn = document.createElement('button');
btn.type = 'button';
btn.id = idBtn;
btn.className = 'btn btn-secondary'; // Dokładnie jak w idea-orders
const label = document.createElement('span');
btn.appendChild(label);
const refresh = () => {
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true);
label.textContent = copyOnlySKU ? 'Kopiuję: SKU' : 'Kopiuję: Pełna nazwa';
btn.title = copyOnlySKU ?
'Kliknij aby przełączyć na kopiowanie pełnej nazwy produktu' :
'Kliknij aby przełączyć na kopiowanie tylko SKU produktu';
log('Odświeżono przycisk, tryb:', copyOnlySKU ? 'SKU' : 'Pełna nazwa');
};
btn.addEventListener('click', () => {
const newMode = !GM_getValue(COPY_MODE_KEY, true);
GM_setValue(COPY_MODE_KEY, newMode);
refresh();
log('Zmieniono tryb kopiowania na:', newMode ? 'SKU' : 'Pełna nazwa');
});
// Dokładnie ta sama logika umieszczania jak w idea-orders.user.js
const massBtn = host.querySelector('button[name="523"]');
if (massBtn && massBtn.parentElement) {
massBtn.insertAdjacentElement('afterend', btn);
log('Dodano przycisk obok button[name="523"]');
} else {
host.appendChild(btn);
log('Dodano przycisk do kontenera:', host.className || host.tagName);
}
refresh();
}
// ——— Menu Tampermonkey ———
GM_registerMenuCommand('Ustawienia KFZ ▶', () => {
const choice = prompt(
'Ustawienia KFZ (wpisz numer):\n' +
'1) Przełącz tryb kopiowania (SKU ⇄ Pełna nazwa)\n' +
'2) Test kopiowania produktów\n' +
'3) Wyświetl aktualny tryb kopiowania'
);
if (!choice) return;
switch (choice.trim()) {
case '1':
const currentMode = GM_getValue(COPY_MODE_KEY, true);
GM_setValue(COPY_MODE_KEY, !currentMode);
const newModeText = !currentMode ? 'SKU' : 'Pełna nazwa';
alert(`Tryb kopiowania zmieniony na: ${newModeText}`);
break;
case '2':
const tables = document.querySelectorAll(TABLE_SEL);
if (tables.length > 0) {
addCopyFunctionality(tables[0]);
alert('Funkcja kopiowania została zastosowana do tabeli KFZ.');
} else {
alert('Nie znaleziono tabeli KFZ na tej stronie.');
}
break;
case '3':
const mode = GM_getValue(COPY_MODE_KEY, true);
alert(`Aktualny tryb kopiowania: ${mode ? 'SKU' : 'Pełna nazwa'}`);
break;
default:
// nic
}
});
// ——— Główna procedura ———
function processOnce() {
log('=== ROZPOCZĘCIE processOnce ===');
if (!isKFZForm()) {
log('Nie jest to formularz KFZ - kończenie');
return;
}
log('Wykryto formularz KFZ, inicjalizacja...');
const tables = document.querySelectorAll(TABLE_SEL);
log(`Znaleziono ${tables.length} tabel z selektorem: ${TABLE_SEL}`);
if (!tables.length) {
// Spróbuj alternatywnych selektorów
const altTables = document.querySelectorAll('table.o_list_table');
log(`Alternatywnie znaleziono ${altTables.length} tabel z selektorem: table.o_list_table`);
const allTables = document.querySelectorAll('table');
log(`Wszystkich tabel na stronie: ${allTables.length}`);
allTables.forEach((table, i) => {
log(`Tabela ${i}:`, table.className);
});
addCopyModeToggleButton();
return;
}
tables.forEach((table, i) => {
log(`Przetwarzanie tabeli ${i}:`, table.className);
addCopyFunctionality(table);
});
// Dodaj kopiowanie dla pól formularza i podsumowania
addFormFieldsCopyFunctionality();
addSummaryFieldsCopyFunctionality();
addCopyModeToggleButton();
log('=== ZAKOŃCZENIE processOnce ===');
}
// ——— Start + reagowanie na zmiany ———
let debounceTimer = null;
const debounce = (fn, ms = 150) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(fn, ms);
};
// Inicjalizacja po załadowaniu DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => debounce(processOnce, 150));
} else {
debounce(processOnce, 150);
}
// Reagowanie na zmiany w DOM i hash
window.addEventListener('hashchange', () => debounce(processOnce, 250));
new MutationObserver(() => debounce(processOnce, 150))
.observe(document.documentElement, { childList: true, subtree: true });
log('Skrypt KFZ załadowany');
})();

View file

@ -0,0 +1,220 @@
// ==UserScript==
// @name IDEAERP - Listy przewozowe
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Kopiowanie elementów na stronach zamówień wysyłkowych - kliknij aby skopiować
// @match https://emma.ideaerp.pl/web*
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
// @downloadURL https://n8n.emma.net.pl/webhook/order-shipping
// @updateURL https://n8n.emma.net.pl/webhook/order-shipping
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// ==/UserScript==
/* ========================OPIS==============================
WERSJA 1.0 - Kopiowanie elementów zamówień wysyłkowych
DZIAŁANIE SKRYPTU:
- Kopiowanie nazwy zamówienia - kliknij na nazwę zamówienia (h1) aby skopiować
- Kopiowanie numeru listu przewozowego - kliknij na element span[name="waybill"] aby skopiować
- Kopiowanie NIP faktury - kliknij na element span[name="invoice_vat"] aby skopiować
- Kopiowanie notatek - kliknij na element span[name="note"] aby skopiować
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
AUTOMATYCZNE AKTUALIZACJE:
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
- Jednoklikowa aktualizacja bez konieczności ręcznego pobierania
- Zachowanie ustawień użytkownika podczas aktualizacji
- Kompatybilność z systemem n8n do zarządzania wersjami
CHANGELOG:
v1.0 (2024) - PIERWSZA WERSJA SKRYPTU WYSYŁKOWEGO
- Utworzono dedykowany skrypt dla zamówień wysyłkowych (shipping.order)
- Dodano kopiowanie nazwy zamówienia (h1) z ikoną
- Dodano kopiowanie numeru listu przewozowego (span[name="waybill"]) z ikoną
- Dodano kopiowanie NIP faktury (span[name="invoice_vat"]) z ikoną
- Dodano kopiowanie notatek (span[name="note"]) z ikoną
- Dodano style CSS dla elementów span z ikonami kopiowania
- Dodano funkcję makeSpanCopyable() dla uniwersalnego kopiowania span
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
- Podwyższenie numeru wersji do 1.0
AUTOR: Adam Grodecki
DATA: 2024
LICENCJA: Własność EMMA SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
================================================================ */
(function() {
'use strict';
// ——— Stałe ———
const STATE_KEY = 'tm_shipping_price_unit_visible';
const INIT_FLAG = 'tm_shipping_init_flag';
const COPY_MODE_KEY = 'tm_shipping_copy_mode';
const WIDTHS_KEY = 'tm_shipping_widths';
// ——— Style CSS ———
GM_addStyle(`
/* Kopiowanie nazwy zamówienia ikona dla h1 */
h1.tm-copyable { cursor: copy; }
h1.tm-copyable::after {
content: '⧉'; /* symbol kopiowania */
display: inline-block; /* w tej samej linii co tekst */
margin-left: 8px; /* odstęp od tekstu */
font-size: 14px; /* większy rozmiar dla h1 */
line-height: 1;
color: #ff9800; /* pomarańczowy */
opacity: .75; /* widoczna także bez hovera */
vertical-align: baseline;
pointer-events: none;
transition: opacity .15s ease;
}
h1.tm-copyable:hover::after { opacity: 1; }
h1.tm-copyable.tm-copied {
outline: 2px solid #ff9800;
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
}
/* Kopiowanie dla elementów span z określonymi name */
span.tm-copyable { cursor: copy; }
span.tm-copyable::after {
content: '⧉'; /* symbol kopiowania */
display: inline-block; /* w tej samej linii co tekst */
margin-left: 6px; /* odstęp od tekstu */
font-size: 12px; /* rozmiar jak dla komórek tabeli */
line-height: 1;
color: #ff9800; /* pomarańczowy */
opacity: .75; /* widoczna także bez hovera */
vertical-align: baseline;
pointer-events: none;
transition: opacity .15s ease;
}
span.tm-copyable:hover::after { opacity: 1; }
span.tm-copyable.tm-copied {
outline: 2px solid #ff9800;
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
}
`);
// ——— Pomocnicze ———
function isShippingOrderForm() {
const params = new URLSearchParams((location.hash || '').replace(/^#/, ''));
const model = params.get('model');
const viewType = params.get('view_type');
return model === 'shipping.order' && viewType === 'form';
}
async function tmCopyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
}
catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
// ——— Kopiowanie nazwy zamówienia ———
function makeOrderNameCopyable(h1Element) {
if (!h1Element || h1Element.dataset.tmOrderNameCopyable) return;
h1Element.dataset.tmOrderNameCopyable = '1';
h1Element.classList.add('tm-copyable');
h1Element.style.cursor = 'pointer';
h1Element.title = 'Kliknij aby skopiować nazwę zamówienia';
h1Element.addEventListener('click', async (e) => {
e.stopPropagation();
const orderName = (h1Element.textContent || '').trim();
if (!orderName) return;
try {
await tmCopyToClipboard(orderName);
const old = h1Element.title;
h1Element.title = `Skopiowano nazwę zamówienia: ${orderName}`;
h1Element.classList.add('tm-copied');
setTimeout(() => {
h1Element.title = old || 'Kliknij aby skopiować nazwę zamówienia';
h1Element.classList.remove('tm-copied');
}, 1200);
} catch (error) {
console.error('[Tampermonkey] Błąd podczas kopiowania nazwy zamówienia:', error);
h1Element.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Kopiowanie elementów span z określonymi name ———
function makeSpanCopyable(spanElement, fieldName) {
if (!spanElement || spanElement.dataset.tmSpanCopyable) return;
spanElement.dataset.tmSpanCopyable = '1';
spanElement.classList.add('tm-copyable');
spanElement.style.cursor = 'pointer';
spanElement.title = `Kliknij aby skopiować ${fieldName}`;
spanElement.addEventListener('click', async (e) => {
e.stopPropagation();
const text = (spanElement.textContent || '').trim();
if (!text) return;
try {
await tmCopyToClipboard(text);
const old = spanElement.title;
spanElement.title = `Skopiowano ${fieldName}: ${text}`;
spanElement.classList.add('tm-copied');
setTimeout(() => {
spanElement.title = old || `Kliknij aby skopiować ${fieldName}`;
spanElement.classList.remove('tm-copied');
}, 1200);
} catch (error) {
console.error(`[Tampermonkey] Błąd podczas kopiowania ${fieldName}:`, error);
spanElement.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Główna procedura ———
function processOnce() {
if (!isShippingOrderForm()) return;
if (!GM_getValue(INIT_FLAG)) { GM_setValue(INIT_FLAG, true); }
// Dodaj kopiowanie nazwy zamówienia
const orderNameSpan = document.querySelector('h1 span[name="name"]');
if (orderNameSpan) {
const orderNameH1 = orderNameSpan.closest('h1');
if (orderNameH1) {
makeOrderNameCopyable(orderNameH1);
}
}
// Dodaj kopiowanie dla elementów span z określonymi name
const waybillSpan = document.querySelector('span[name="waybill"]');
if (waybillSpan) {
makeSpanCopyable(waybillSpan, 'numer listu przewozowego');
}
const invoiceVatSpan = document.querySelector('span[name="invoice_vat"]');
if (invoiceVatSpan) {
makeSpanCopyable(invoiceVatSpan, 'NIP faktury');
}
const noteSpan = document.querySelector('span[name="note"]');
if (noteSpan) {
makeSpanCopyable(noteSpan, 'notatkę');
}
}
// ——— Start + reagowanie na zmiany ———
let deb = null; const debounce = (fn, ms = 150) => { clearTimeout(deb); deb = setTimeout(fn, ms); };
window.addEventListener('load', () => debounce(processOnce, 150));
window.addEventListener('hashchange', () => debounce(processOnce, 250));
new MutationObserver(() => debounce(processOnce, 150)).observe(document.documentElement, { childList: true, subtree: true });
})();

758
idea/idea-orders.user.js Executable file
View file

@ -0,0 +1,758 @@
// ==UserScript==
// @name IDEAERP - Zamówienia
// @namespace http://tampermonkey.net/
// @version 3.7
// @description Dodano kopiowanie nazwy zamówienia, NIP faktury i notatek - kliknij aby skopiować
// @match https://emma.ideaerp.pl/web*
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
// @downloadURL https://n8n.emma.net.pl/webhook/order
// @updateURL https://n8n.emma.net.pl/webhook/order
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// ==/UserScript==
/* ========================OPIS==============================
WERSJA 3.7 - Rozszerzone kopiowanie elementów
WYMAGANE KOLUMNY w tabeli IdeaERP:
- product_uom_qty Ilość
- order_line_netto Netto (lub price_subtotal jako zamiennik)
- order_line_gross Brutto
- price_unit Cena jednostkowa
- product_id Produkt
DZIAŁANIE SKRYPTU:
- Wszystkie funkcje z v3.6 PLUS:
- Kopiowanie nazwy zamówienia - kliknij na nazwę zamówienia (h1) aby skopiować
- Kopiowanie NIP faktury - kliknij na element span[name="invoice_vat"] aby skopiować
- Kopiowanie notatek - kliknij na element span[name="note"] aby skopiować
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
- Naprawione wyświetlanie kolumny "Cena jednostkowa" po włączeniu
- Lepsze zarządzanie szerokościami tabeli
- Optymalizacja layoutu dla różnych rozdzielczości
POPRAWKI WZGLĘDEM v3.1:
+ Naprawione zarządzanie szerokością tabeli (table-layout: auto !important)
+ Zmniejszone domyślne szerokości kolumn (90px zamiast 120px)
+ Ulepszona funkcja setPriceUnitVisibility() z lepszym zarządzaniem stylów
+ Konkretne wymiary dla kolumny "Cena jednostkowa" (120px-180px)
+ Wymuszenie elastycznego layoutu przy pokazywaniu kolumny
+ Czyszczenie stylów komórek przy przełączaniu widoczności
+ Dodatkowe zabezpieczenia w processOnce() dla table-layout
ROZWIĄZANE PROBLEMY:
- Kolumna "Cena jednostkowa" nie była widoczna po kliknięciu "Pokaż"
- Konflikty między table-layout: fixed a dynamicznymi kolumnami
- Problemy z szerokościami na różnych rozdzielczościach
- Brak miejsca w tabeli po dodaniu nowych kolumn
ZAPAMIĘTYWANIE USTAWIEŃ:
- Wszystkie ustawienia (widoczność kolumn, tryb kopiowania, szerokości)
zapisywane w trwałej pamięci przeglądarki.
- Ustawienia przetrwają: restart komputera, zamknięcie przeglądarki,
odświeżenie strony, aktualizację przeglądarki.
- Ustawienia zostaną utracone tylko przy: usunięciu skryptu,
wyczyszczeniu danych przeglądarki lub zmianie komputera.
AUTOMATYCZNE AKTUALIZACJE:
- Serwer aktualizacji: https://n8n.emma.net.pl/webhook/order
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
- Jednoklikowa aktualizacja bez konieczności ręcznego pobierania
- Zachowanie ustawień użytkownika podczas aktualizacji
- Kompatybilność z systemem n8n do zarządzania wersjami
CHANGELOG:
v3.7 (2024) - ROZSZERZONE KOPIOWANIE ELEMENTÓW
- Dodano kopiowanie nazwy zamówienia (h1) z ikoną
- Dodano kopiowanie NIP faktury (span[name="invoice_vat"]) z ikoną
- Dodano kopiowanie notatek (span[name="note"]) z ikoną
- Dodano style CSS dla elementów span z ikonami kopiowania
- Dodano funkcję makeSpanCopyable() dla uniwersalnego kopiowania span
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
- Podwyższenie numeru wersji do 3.7
v3.6 (2024) - KOPIOWANIE NAZWY ZAMÓWIENIA
- Dodano kopiowanie nazwy zamówienia z ikoną
- Dodano style CSS dla elementu h1 z ikoną kopiowania
- Dodano funkcję makeOrderNameCopyable() z obsługą błędów
- Poprawiono selektor elementu (h1 zamiast span)
- Podwyższenie numeru wersji do 3.6
v3.5 (2024) - AUTOMATYCZNE AKTUALIZACJE
- Dodano dokumentację automatycznych aktualizacji w sekcji OPIS
- Dodano sekcję "AUTOMATYCZNE AKTUALIZACJE" z opisem funkcjonalności
- Dokumentacja serwera aktualizacji (n8n.emma.net.pl/webhook/order)
- Informacja o zachowaniu ustawień podczas aktualizacji
- Podwyższenie numeru wersji do 3.5
v3.4 (2024) - STABILIZACJA
- Stabilna wersja z pełną funkcjonalnością
- Wszystkie poprzednie poprawki zintegrowane
- Gotowość do automatycznych aktualizacji
v3.3 (2024) - DROBNE POPRAWKI
- Dodatkowe optymalizacje wydajności
- Poprawki w obsłudze błędów
- Ulepszenia w interfejsie użytkownika
v3.2 (2024) - POPRAWKA LAYOUTU
- Naprawione zarządzanie szerokością tabeli (table-layout: auto !important)
- Zmniejszone domyślne szerokości kolumn (90px zamiast 120px)
- Ulepszona funkcja setPriceUnitVisibility() z lepszym zarządzaniem stylów
- Konkretne wymiary dla kolumny "Cena jednostkowa" (120px-180px)
- Wymuszenie elastycznego layoutu przy pokazywaniu kolumny
- Czyszczenie stylów komórek przy przełączaniu widoczności
- Dodatkowe zabezpieczenia w processOnce() dla table-layout
- Rozwiązano problem z niewidoczną kolumną "Cena jednostkowa"
- Optymalizacja dla różnych rozdzielczości ekranu
============================================================ */
(function () {
'use strict';
// ——— Klucze stanu ———
const STATE_KEY = 'tm_show_price_unit_column_v41'; // true = widoczna, false = ukryta (domyślnie false)
const WIDTHS_KEY = 'tm_col_widths_v41';
const INIT_FLAG = 'tm_inited_v41';
const COPY_MODE_KEY = 'tm_product_copy_mode_v31'; // true = SKU, false = pełna nazwa (domyślnie true)
const TABLE_SEL = 'table.o_list_table.o_section_and_note_list_view';
GM_addStyle(`
/* Poprawka dla tabeli - pozwolenie na elastyczne szerokości */
${TABLE_SEL} { table-layout: auto !important; }
th.tm-col, td.tm-col { text-align: right !important; font-size: 13px !important; }
th.tm-col { min-width: 90px !important; position: relative; }
th.tm-col .tm-resize { position: absolute; right: 0; top: 0; bottom: 0; width: 6px; cursor: col-resize; }
th.tm-col.tm-sort-asc::after { content: ' ▲'; font-size: 10px; }
th.tm-col.tm-sort-desc::after { content: ' ▼'; font-size: 10px; }
/* Zmniejszona szerokość kolumny price_unit dla lepszego dopasowania */
th[data-name="price_unit"] {
min-width: 120px !important;
width: 120px !important;
max-width: 180px !important;
}
th[data-name="price_unit"] .custom-tm-span-text { color: orange; font-weight: 600; }
/* Dodatkowe zabezpieczenie ukrywania po atrybucie */
table.tm-hide-price-unit th[data-name="price_unit"],
table.tm-hide-price-unit td[data-name="price_unit"] { display: none !important; }
/* 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));
}
/* Kopiowanie nazwy zamówienia ikona dla h1 */
h1.tm-copyable { cursor: copy; }
h1.tm-copyable::after {
content: '⧉'; /* symbol kopiowania */
display: inline-block; /* w tej samej linii co tekst */
margin-left: 8px; /* odstęp od tekstu */
font-size: 14px; /* większy rozmiar dla h1 */
line-height: 1;
color: #ff9800; /* pomarańczowy */
opacity: .75; /* widoczna także bez hovera */
vertical-align: baseline;
pointer-events: none;
transition: opacity .15s ease;
}
h1.tm-copyable:hover::after { opacity: 1; }
h1.tm-copyable.tm-copied {
outline: 2px solid #ff9800;
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
}
/* Kopiowanie dla elementów span z określonymi name */
span.tm-copyable { cursor: copy; }
span.tm-copyable::after {
content: '⧉'; /* symbol kopiowania */
display: inline-block; /* w tej samej linii co tekst */
margin-left: 6px; /* odstęp od tekstu */
font-size: 12px; /* rozmiar jak dla komórek tabeli */
line-height: 1;
color: #ff9800; /* pomarańczowy */
opacity: .75; /* widoczna także bez hovera */
vertical-align: baseline;
pointer-events: none;
transition: opacity .15s ease;
}
span.tm-copyable:hover::after { opacity: 1; }
span.tm-copyable.tm-copied {
outline: 2px solid #ff9800;
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
}
`);
// ——— Pomocnicze ———
const nfPL = new Intl.NumberFormat('pl-PL', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const fmt = n => (isFinite(n) ? nfPL.format(n) : '');
function isOrderForm() {
const params = new URLSearchParams((location.hash || '').replace(/^#/, ''));
const model = params.get('model');
const viewType = params.get('view_type');
return (model === 'sale.order' || model === 'shipping.order') && viewType === 'form';
}
function parseNumber(str) {
if (!str) return NaN;
return parseFloat(String(str)
.replace(/[\u00A0\u202F]/g, '') // nbsp/thin space
.replace(/[^\d,.-]/g, '')
.replace(/\.(?=\d{3}(\D|$))/g, '') // kropki tys.
.replace(/,(?=\d{0,2}$)/, '.')); // przecinek -> kropka
}
const headerRow = t => (t && t.tHead) ? t.tHead.rows[t.tHead.rows.length - 1] : t.querySelector('thead tr');
const thIndex = (t, name) => { const hr = headerRow(t); if (!hr) return -1; const arr = Array.from(hr.children); for (let i = 0; i < arr.length; i++) if (arr[i].getAttribute('data-name') === name) return i; return -1; };
const headerMap = t => { const hr = headerRow(t); const m = {}; if (!hr) return m; Array.from(hr.children).forEach((th, i) => { const dn = th.getAttribute('data-name'); if (dn) m[dn] = i; }); return m; };
const cellAt = (row, i) => { const c = row && row.children; return (c && i >= 0 && i < c.length) ? c[i] : null; };
const textOf = el => el ? (el.getAttribute('title') || el.textContent || '').trim() : '';
// ——— Funkcja wyodrębniania SKU z nawiasów kwadratowych ———
function extractSKU(productText) {
const match = productText.match(/\[([^\]]+)\]/);
return match ? match[1] : productText;
}
// ——— Schowek: kopiowanie wartości z komórek ———
async function tmCopyToClipboard(text) {
try { await navigator.clipboard.writeText(text); }
catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
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
const val = (td.textContent || '').trim();
if (!val) return;
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);
} catch (error) {
console.error('[Tampermonkey] Błąd podczas kopiowania komórki:', error);
td.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Kopiowanie produktów z trybem SKU/pełna nazwa ———
function makeProductCellCopyable(td) {
if (!td || td.dataset.tmProductCopyable) return;
td.dataset.tmProductCopyable = '1';
td.classList.add('tm-copyable');
td.addEventListener('click', async (e) => {
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
const fullText = (td.textContent || '').trim();
if (!fullText) return;
try {
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true); // domyślnie SKU
const textToCopy = copyOnlySKU ? extractSKU(fullText) : fullText;
await tmCopyToClipboard(textToCopy);
const old = td.title;
const mode = copyOnlySKU ? 'SKU' : 'pełną nazwę';
td.title = `Skopiowano ${mode}: ${textToCopy}`;
td.classList.add('tm-copied');
setTimeout(() => { td.title = old || 'Skopiowano'; td.classList.remove('tm-copied'); }, 1200);
} catch (error) {
console.error('[Tampermonkey] Błąd podczas kopiowania produktu:', error);
td.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Kopiowanie nazwy zamówienia ———
function makeOrderNameCopyable(h1Element) {
if (!h1Element || h1Element.dataset.tmOrderNameCopyable) return;
h1Element.dataset.tmOrderNameCopyable = '1';
h1Element.classList.add('tm-copyable');
h1Element.style.cursor = 'pointer';
h1Element.title = 'Kliknij aby skopiować nazwę zamówienia';
h1Element.addEventListener('click', async (e) => {
e.stopPropagation();
const orderName = (h1Element.textContent || '').trim();
if (!orderName) return;
try {
await tmCopyToClipboard(orderName);
const old = h1Element.title;
h1Element.title = `Skopiowano nazwę zamówienia: ${orderName}`;
h1Element.classList.add('tm-copied');
setTimeout(() => {
h1Element.title = old || 'Kliknij aby skopiować nazwę zamówienia';
h1Element.classList.remove('tm-copied');
}, 1200);
} catch (error) {
console.error('[Tampermonkey] Błąd podczas kopiowania nazwy zamówienia:', error);
h1Element.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Kopiowanie elementów span z określonymi name ———
function makeSpanCopyable(spanElement, fieldName) {
if (!spanElement || spanElement.dataset.tmSpanCopyable) return;
spanElement.dataset.tmSpanCopyable = '1';
spanElement.classList.add('tm-copyable');
spanElement.style.cursor = 'pointer';
spanElement.title = `Kliknij aby skopiować ${fieldName}`;
spanElement.addEventListener('click', async (e) => {
e.stopPropagation();
const text = (spanElement.textContent || '').trim();
if (!text) return;
try {
await tmCopyToClipboard(text);
const old = spanElement.title;
spanElement.title = `Skopiowano ${fieldName}: ${text}`;
spanElement.classList.add('tm-copied');
setTimeout(() => {
spanElement.title = old || `Kliknij aby skopiować ${fieldName}`;
spanElement.classList.remove('tm-copied');
}, 1200);
} catch (error) {
console.error(`[Tampermonkey] Błąd podczas kopiowania ${fieldName}:`, error);
spanElement.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Nagłówki (nasze, bez Odoo) ———
function ensureCustomHeaders(table) {
const hr = headerRow(table); if (!hr) return;
let pIdx = thIndex(table, 'price_unit'); if (pIdx < 0) return;
if (!table.querySelector('th[data-name="tm_unit_netto"]')) {
const th = document.createElement('th');
th.setAttribute('data-name', 'tm_unit_netto');
th.className = 'tm-col';
th.textContent = 'Cena jedn. netto';
th.title = 'Cena jedn. netto = Netto / Ilość';
th.appendChild(document.createElement('span')).className = 'tm-resize';
const ref = cellAt(hr, pIdx + 1); ref ? hr.insertBefore(th, ref) : hr.appendChild(th);
}
pIdx = thIndex(table, 'price_unit');
if (!table.querySelector('th[data-name="tm_unit_brutto"]')) {
const th = document.createElement('th');
th.setAttribute('data-name', 'tm_unit_brutto');
th.className = 'tm-col';
th.textContent = 'Cena jedn. brutto';
th.title = 'Cena jedn. brutto = Cena jedn. netto × (Brutto/Netto)';
th.appendChild(document.createElement('span')).className = 'tm-resize';
const ref = cellAt(hr, pIdx + 2); ref ? hr.insertBefore(th, ref) : hr.appendChild(th);
}
}
// ——— Komórki wierszy ———
function ensureRowCells(row, priceIdx) {
let tdN = row.querySelector('td[data-name="tm_unit_netto"]');
if (!tdN) {
tdN = document.createElement('td');
tdN.setAttribute('data-name', 'tm_unit_netto');
tdN.className = 'tm-col o_data_cell o_field_cell o_list_number';
tdN.tabIndex = -1; tdN.title = 'Cena jedn. netto';
const ref = cellAt(row, priceIdx + 1); ref ? row.insertBefore(tdN, ref) : row.appendChild(tdN);
}
let tdB = row.querySelector('td[data-name="tm_unit_brutto"]');
if (!tdB) {
tdB = document.createElement('td');
tdB.setAttribute('data-name', 'tm_unit_brutto');
tdB.className = 'tm-col o_data_cell o_field_cell o_list_number';
tdB.tabIndex = -1; tdB.title = 'Cena jedn. brutto';
const ref2 = cellAt(row, priceIdx + 2); ref2 ? row.insertBefore(tdB, ref2) : row.appendChild(tdB);
}
// Uczyń kopiowalnymi
makeCellCopyable(tdN);
makeCellCopyable(tdB);
}
function ensureAllRowCells(table, priceIdx) {
table.querySelectorAll('tbody tr').forEach(tr => ensureRowCells(tr, priceIdx));
}
// ——— Kopiowanie produktów ———
function ensureProductCopyable(table, map) {
const prodIdx = map["product_id"]; // kolumna Produkt
if (prodIdx == null) return;
table.querySelectorAll("tbody tr").forEach((tr) => {
const td = cellAt(tr, prodIdx);
if (td) makeProductCellCopyable(td);
});
}
// ——— Wyliczenia + chmurki ———
function tagPriceUnitHeader(table, map) {
const th = table.querySelector('th[data-name="price_unit"]'); if (!th) return;
const row = table.querySelector('tbody tr'); if (!row) return;
const qty = parseNumber(textOf(cellAt(row, map['product_uom_qty'])));
const net = parseNumber(textOf(cellAt(row, map['order_line_netto'] ?? map['price_subtotal'])));
const unit = parseNumber(textOf(cellAt(row, map['price_unit'])));
if (!isFinite(qty) || qty <= 0 || !isFinite(net) || !isFinite(unit)) return;
const unitNet = net / qty;
const txt = (Math.abs(unitNet - unit) < 0.0001) ? 'netto' : (unitNet < unit ? 'brutto' : 'jedn.');
let span = th.querySelector('.custom-tm-span-text');
if (!span) { th.appendChild(document.createTextNode(' ')); span = document.createElement('span'); span.className = 'custom-tm-span-text'; th.appendChild(span); }
span.textContent = txt; th.title = `Cena jednostkowa ${txt}`;
}
function fillValues(table) {
const map = headerMap(table);
tagPriceUnitHeader(table, map);
const qtyIdx = map['product_uom_qty'];
const netIdx = map['order_line_netto'] ?? map['price_subtotal'];
const grossIdx = map['order_line_gross'];
table.querySelectorAll('tbody tr').forEach(row => {
const qty = parseNumber(textOf(cellAt(row, qtyIdx)));
const net = parseNumber(textOf(cellAt(row, netIdx)));
const gross = parseNumber(textOf(cellAt(row, grossIdx)));
const outN = row.querySelector('td[data-name="tm_unit_netto"]');
const outB = row.querySelector('td[data-name="tm_unit_brutto"]');
if (!isFinite(qty) || qty <= 0 || !isFinite(net) || net <= 0) {
if (outN) { outN.textContent = ''; outN.title = 'Cena jedn. netto'; }
if (outB) { outB.textContent = ''; outB.title = 'Cena jedn. brutto'; }
return;
}
const unitNet = net / qty;
if (outN) {
outN.textContent = fmt(unitNet);
outN.title = `Cena jedn. netto = ${fmt(net)} / ${fmt(qty)} = ${fmt(unitNet)}`;
makeCellCopyable(outN);
}
let ratio = null; // Brutto/Netto
if (isFinite(gross) && gross > 0) { const r = gross / net; if (r >= 1 && r < 5) ratio = r; }
if (ratio == null) {
const shown = parseNumber(textOf(cellAt(row, map['price_unit'])));
if (shown > unitNet + 0.0001) { const r2 = shown / unitNet; if (r2 > 1 && r2 < 5) ratio = r2; }
}
if (outB) {
if (ratio != null) {
const ug = unitNet * ratio;
outB.textContent = fmt(ug);
outB.title = `Cena jedn. brutto = ${fmt(unitNet)} × ${ratio.toFixed(2)} = ${fmt(ug)}`;
} else {
outB.textContent = ''; outB.title = 'Cena jedn. brutto';
}
makeCellCopyable(outB);
}
});
}
// ——— Sortowanie (tylko nasze kolumny) ———
function bindSort(th, table) {
th.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation();
const hr = headerRow(table); const idx = Array.prototype.indexOf.call(hr.children, th);
const asc = !th.classList.contains('tm-sort-asc');
table.querySelectorAll('th.tm-col').forEach(x => x.classList.remove('tm-sort-asc', 'tm-sort-desc'));
th.classList.add(asc ? 'tm-sort-asc' : 'tm-sort-desc');
const tbody = table.tBodies && table.tBodies[0]; if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr.o_data_row, tr')).filter(r => r.querySelector('td'));
rows.sort((a, b) => {
const va = parseNumber(textOf(cellAt(a, idx)));
const vb = parseNumber(textOf(cellAt(b, idx)));
const an = isFinite(va), bn = isFinite(vb);
if (an && bn) return asc ? (va - vb) : (vb - va);
if (an && !bn) return -1; if (!an && bn) return 1; return 0;
});
rows.forEach(r => tbody.appendChild(r));
}, true);
}
// ——— Zmiana szerokości ———
function loadWidths() { try { return JSON.parse(GM_getValue(WIDTHS_KEY, '{}')) || {}; } catch { return {}; } }
function saveWidths(w) { GM_setValue(WIDTHS_KEY, JSON.stringify(w || {})); }
function setWidthPx(table, th, px) {
const w = Math.max(60, Math.round(px));
th.style.width = w + 'px'; th.style.minWidth = w + 'px'; th.style.maxWidth = w + 'px';
const hr = headerRow(table); const idx = Array.prototype.indexOf.call(hr.children, th);
table.querySelectorAll('tbody tr, tfoot tr').forEach(tr => { const td = cellAt(tr, idx); if (td) { td.style.width = w + 'px'; td.style.minWidth = w + 'px'; td.style.maxWidth = w + 'px'; } });
}
function bindResize(th, table) {
const h = th.querySelector('.tm-resize'); if (!h) return;
let sx = 0, sw = 0, moving = false;
const down = (e) => { e.preventDefault(); e.stopPropagation(); moving = true; sx = e.pageX; sw = th.getBoundingClientRect().width; document.addEventListener('mousemove', move, true); document.addEventListener('mouseup', up, true); document.body.style.userSelect = 'none'; };
const move = (e) => { if (!moving) return; setWidthPx(table, th, sw + (e.pageX - sx)); };
const up = () => { if (!moving) return; moving = false; document.removeEventListener('mousemove', move, true); document.removeEventListener('mouseup', up, true); document.body.style.userSelect = ''; const w = loadWidths(); w[th.getAttribute('data-name')] = Math.round(th.getBoundingClientRect().width); saveWidths(w); };
h.addEventListener('mousedown', down, true);
}
function applyWidthsAndBind(table) {
['tm_unit_netto', 'tm_unit_brutto'].forEach(name => {
const th = table.querySelector(`th[data-name="${name}"]`); if (!th) return;
const saved = loadWidths()[name]; if (saved) setWidthPx(table, th, saved);
if (!th.dataset.tmBound) { th.dataset.tmBound = '1'; bindResize(th, table); bindSort(th, table); }
});
}
// ——— Ulepszona funkcja zarządzania widocznością kolumny "Cena jednostkowa" ———
function setPriceUnitVisibility(table, visible) {
if (visible) table.classList.remove('tm-hide-price-unit'); else table.classList.add('tm-hide-price-unit');
const hr = headerRow(table); if (!hr) return;
const th = table.querySelector('th[data-name="price_unit"]'); if (!th) return;
const idx = Array.prototype.indexOf.call(hr.children, th);
if (visible) {
// Pokazujemy kolumnę - przywracamy normalną szerokość
th.style.display = '';
th.style.width = '120px';
th.style.minWidth = '120px';
th.style.maxWidth = '180px';
// Przywracamy widoczność komórek w tej kolumnie
table.querySelectorAll('tbody tr, tfoot tr').forEach(tr => {
const c = cellAt(tr, idx);
if (c) {
c.style.display = '';
c.style.width = '';
c.style.minWidth = '';
c.style.maxWidth = '';
}
});
// Upewniamy się, że tabela ma elastyczny layout
table.style.tableLayout = 'auto';
} else {
// Ukrywamy kolumnę
th.style.display = 'none';
th.style.width = '0';
th.style.minWidth = '0';
th.style.maxWidth = '0';
table.querySelectorAll('tbody tr, tfoot tr').forEach(tr => {
const c = cellAt(tr, idx);
if (c) c.style.display = 'none';
});
}
}
function applyVisibilityAll(visible) { document.querySelectorAll(TABLE_SEL).forEach(t => setPriceUnitVisibility(t, visible)); }
// ——— Lokalizowanie docelowego miejsca dla przycisku ———
function findActionsContainer() {
const byId = document.getElementById('notebook_page_130');
if (byId) return byId;
const pricelistBtn = document.querySelector('button[name="load_from_pricelist"]');
if (pricelistBtn) return pricelistBtn.closest('.tab-pane') || pricelistBtn.parentElement;
return null;
}
// ——— Przycisk Pokaż/Ukryj (btn btn-secondary) ———
function addToggleButton() {
const host = findActionsContainer();
if (!host) return;
const idBtn = 'tm-toggle-main-button';
if (host.querySelector('#' + idBtn)) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.id = idBtn;
btn.className = 'btn btn-secondary';
const label = document.createElement('span');
btn.appendChild(label);
const refresh = () => { const v = !!GM_getValue(STATE_KEY, false); label.textContent = `${v ? 'Ukryj' : 'Pokaż'} Cenę Jednostkową`; };
btn.addEventListener('click', () => {
const v = !GM_getValue(STATE_KEY, false);
GM_setValue(STATE_KEY, v);
applyVisibilityAll(v);
refresh();
});
const massBtn = host.querySelector('button[name="523"]');
if (massBtn && massBtn.parentElement) massBtn.insertAdjacentElement('afterend', btn);
else host.appendChild(btn);
refresh();
}
// ——— Przycisk przełączania trybu kopiowania produktów ———
function addCopyModeToggleButton() {
const host = findActionsContainer();
if (!host) return;
const idBtn = 'tm-copy-mode-toggle-button';
if (host.querySelector('#' + idBtn)) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.id = idBtn;
btn.className = 'btn btn-secondary';
const label = document.createElement('span');
btn.appendChild(label);
const refresh = () => {
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true);
label.textContent = copyOnlySKU ? 'Kopiuję: SKU' : 'Kopiuję: Pełna nazwa';
btn.title = copyOnlySKU ? 'Kliknij aby przełączyć na kopiowanie pełnej nazwy produktu' : 'Kliknij aby przełączyć na kopiowanie tylko SKU produktu';
};
btn.addEventListener('click', () => {
const newMode = !GM_getValue(COPY_MODE_KEY, true);
GM_setValue(COPY_MODE_KEY, newMode);
refresh();
});
// Umieść przycisk obok pierwszego przycisku
const mainToggleBtn = host.querySelector('#tm-toggle-main-button');
if (mainToggleBtn && mainToggleBtn.parentElement) {
mainToggleBtn.insertAdjacentElement('afterend', btn);
} else {
const massBtn = host.querySelector('button[name="523"]');
if (massBtn && massBtn.parentElement) massBtn.insertAdjacentElement('afterend', btn);
else host.appendChild(btn);
}
refresh();
}
// ——— Menu Tampermonkey (podmenu) ———
GM_registerMenuCommand('Ustawienia ▶', () => {
const choice = prompt(
'Ustawienia (wpisz numer):\n' +
'1) Pokaż kolumnę „Cena jednostkowa"\n' +
'2) Ukryj kolumnę „Cena jednostkowa"\n' +
'3) Zresetuj szerokości netto/brutto\n' +
'4) Wyczyść stan (domyślnie ukryta)\n' +
'5) Test kopiowania produktów\n' +
'6) Przełącz tryb kopiowania (SKU ⇄ Pełna nazwa)'
);
if (!choice) return;
switch (choice.trim()) {
case '1':
GM_setValue(STATE_KEY, true);
applyVisibilityAll(true);
alert('Kolumna widoczna.');
break;
case '2':
GM_setValue(STATE_KEY, false);
applyVisibilityAll(false);
alert('Kolumna ukryta.');
break;
case '3':
GM_setValue(WIDTHS_KEY, '{}');
alert('Zresetowano szerokości. Odśwież widok.');
break;
case '4':
GM_deleteValue(STATE_KEY);
applyVisibilityAll(false);
alert('Wyczyszczono. Kolumna ustawiona na: ukryta.');
break;
case '5':
const tables = document.querySelectorAll(TABLE_SEL);
if (tables.length > 0) {
const table = tables[0];
const map = headerMap(table);
ensureProductCopyable(table, map);
alert('Funkcja kopiowania produktów została zastosowana do tabeli.');
} else {
alert('Nie znaleziono tabeli zamówień na tej stronie.');
}
break;
case '6':
const currentMode = GM_getValue(COPY_MODE_KEY, true);
GM_setValue(COPY_MODE_KEY, !currentMode);
const newModeText = !currentMode ? 'SKU' : 'Pełna nazwa';
alert(`Tryb kopiowania zmieniony na: ${newModeText}`);
break;
default:
// nic
}
});
// ——— Główna procedura ———
function processOnce() {
if (!isOrderForm()) return;
const tables = document.querySelectorAll(TABLE_SEL); if (!tables.length) { addToggleButton(); addCopyModeToggleButton(); return; }
if (!GM_getValue(INIT_FLAG)) { GM_setValue(STATE_KEY, false); GM_setValue(INIT_FLAG, true); }
// Dodaj kopiowanie nazwy zamówienia
const orderNameSpan = document.querySelector('h1 span[name="name"]');
if (orderNameSpan) {
const orderNameH1 = orderNameSpan.closest('h1');
if (orderNameH1) {
makeOrderNameCopyable(orderNameH1);
}
}
// Dodaj kopiowanie dla elementów span z określonymi name
const invoiceVatSpan = document.querySelector('span[name="invoice_vat"]');
if (invoiceVatSpan) {
makeSpanCopyable(invoiceVatSpan, 'NIP faktury');
}
const noteSpan = document.querySelector('span[name="note"]');
if (noteSpan) {
makeSpanCopyable(noteSpan, 'notatkę');
}
tables.forEach(table => {
// Upewniamy się, że tabela ma elastyczny layout
table.style.tableLayout = 'auto';
ensureCustomHeaders(table);
const pIdx = thIndex(table, 'price_unit'); if (pIdx < 0) return;
ensureAllRowCells(table, pIdx);
fillValues(table);
// Dodaj kopiowanie produktów
const map = headerMap(table);
ensureProductCopyable(table, map);
setPriceUnitVisibility(table, !!GM_getValue(STATE_KEY, false));
applyWidthsAndBind(table);
});
addToggleButton();
addCopyModeToggleButton();
}
// ——— Start + reagowanie na zmiany ———
let deb = null; const debounce = (fn, ms = 150) => { clearTimeout(deb); deb = setTimeout(fn, ms); };
window.addEventListener('load', () => debounce(processOnce, 150));
window.addEventListener('hashchange', () => debounce(processOnce, 250));
new MutationObserver(() => debounce(processOnce, 150)).observe(document.documentElement, { childList: true, subtree: true });
})();

View file

@ -0,0 +1,308 @@
// ==UserScript==
// @name IDEAERP - Rejestracja Płatności
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Dodano kopiowanie kwoty w oknie rejestracji płatności
// @match https://emma.ideaerp.pl/web*
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
// @downloadURL https://n8n.emma.net.pl/webhook/idea-payment-register
// @updateURL https://n8n.emma.net.pl/webhook/idea-payment-register
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// ==/UserScript==
/* ========================OPIS==============================
WERSJA 1.0 - Kopiowanie kwoty płatności
DZIAŁANIE SKRYPTU:
- Wykrywa okno modalne "Rejestruj płatność"
- Dodaje ikonę kopiowania () przy polu "Kwota" (amount)
- Kliknięcie kopiuje wartość do schowka
- Obsługa błędów i logowanie
CHANGELOG:
v1.0 (2024) - PIERWSZA WERSJA
- Dodano obsługę okna modalnego
- Dodano kopiowanie pola amount
- Stylizacja zgodna z innymi skryptami
============================================================ */
(function () {
'use strict';
// ——— Style CSS ———
GM_addStyle(`
/* Kopiowanie ikona tuż za liczbą (inline), mniejsza, pomarańczowa */
.tm-copyable-icon {
cursor: copy;
display: inline-block;
margin-left: 8px;
font-size: 14px;
line-height: 1;
color: #ff9800;
opacity: .75;
vertical-align: middle;
transition: opacity .15s ease;
}
.tm-copyable-icon:hover { opacity: 1; }
.tm-copied {
color: #4caf50 !important; /* Zielony po skopiowaniu */
}
`);
// ——— Schowek ———
async function tmCopyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('[Tampermonkey] Clipboard API failed, trying fallback', err);
try {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
return true;
} catch (e) {
console.error('[Tampermonkey] Fallback copy failed', e);
return false;
}
}
}
// ——— Główna logika ———
function processModal(modal) {
if (modal.dataset.tmProcessed) return;
// Sprawdź czy to modal płatności
const title = modal.querySelector('.modal-title');
const isPaymentModal = title && (title.textContent.includes('Rejestruj płatność') || title.textContent.includes('Register Payment'));
if (!isPaymentModal) {
// Jeśli nie ma tytułu, sprawdzamy czy są pola specyficzne dla płatności
if (!modal.querySelector('input[name="amount"]')) return;
}
console.log('[Tampermonkey] Przetwarzanie modalu płatności...');
// Lista pól do obsłużenia
const fieldsToProcess = [
{ name: 'amount', label: 'Kwota' },
{ name: 'amount_div', label: 'Kwota' }, // Fallback dla struktury z amount_div
{ name: 'payment_date', label: 'Data płatności' },
{ name: 'communication', label: 'Notatka' },
{ name: 'ref', label: 'Referencja' }
];
fieldsToProcess.forEach(field => {
// Priorytet dla inputa - szukamy najpierw inputa
let element = modal.querySelector(`input[name="${field.name}"]`);
// Jeśli nie ma inputa, szukamy div lub span
if (!element) {
element = modal.querySelector(`div[name="${field.name}"], span[name="${field.name}"]`);
}
if (element) {
console.log(`[Tampermonkey] Znaleziono pole ${field.name}:`, element);
addCopyIcon(element, field.label);
// Dla pola kwoty dodaj dodatkowy wiersz z VAT
if (field.name === 'amount' || field.name === 'amount_div') {
addVatRow(element, modal);
}
}
});
modal.dataset.tmProcessed = '1';
}
function addVatRow(amountElement, modal) {
// Znajdź wiersz tabeli (tr) zawierający pole kwoty
const amountRow = amountElement.closest('tr');
if (!amountRow) return;
// Sprawdź czy wiersz VAT już istnieje
if (amountRow.nextElementSibling && amountRow.nextElementSibling.classList.contains('tm-vat-row')) return;
// Ustal element z wartością (input) - priorytet dla inputa wewnątrz kontenera
let valueElement = amountElement;
if (amountElement.tagName !== 'INPUT' && amountElement.querySelector('input')) {
valueElement = amountElement.querySelector('input');
}
const getAmountValue = () => {
let valStr = '';
if (valueElement.tagName === 'INPUT') {
valStr = valueElement.value;
} else {
valStr = valueElement.textContent;
}
// Używamy tej samej logiki czyszczenia co przy kopiowaniu
valStr = valStr.replace(/[^\d,.-]/g, '').replace(',', '.');
return parseFloat(valStr) || 0;
};
// Oblicz VAT (Brutto -> VAT)
// Wzór: VAT = Brutto * 23 / 123
const calculateVat = (val) => {
if (!val) return '0,00';
const vat = (val * 23) / 123;
return vat.toFixed(2).replace('.', ',');
};
const amountVal = getAmountValue();
const calculatedValue = calculateVat(amountVal);
// Stwórz nowy wiersz
const tr = document.createElement('tr');
tr.className = 'tm-vat-row';
tr.innerHTML = `
<td class="o_td_label">
<label class="o_form_label">Kwota VAT (obl.)</label>
</td>
<td style="width: 100%; display: flex; align-items: center;">
<span class="tm-vat-value" style="margin-right: 5px; font-weight: bold;">${calculatedValue}</span>
<span class="tm-copyable-icon" title="Kliknij aby skopiować Kwota VAT"></span>
</td>
`;
// Wstaw za wierszem kwoty
amountRow.parentNode.insertBefore(tr, amountRow.nextSibling);
// Obsługa kopiowania dla nowego pola
const copyIcon = tr.querySelector('.tm-copyable-icon');
const valueSpan = tr.querySelector('.tm-vat-value');
copyIcon.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const val = valueSpan.textContent;
if (val) {
const success = await tmCopyToClipboard(val);
if (success) {
copyIcon.classList.add('tm-copied');
setTimeout(() => copyIcon.classList.remove('tm-copied'), 1200);
}
}
});
// Nasłuchiwanie zmian w polu kwoty (jeśli to input)
if (valueElement.tagName === 'INPUT') {
valueElement.addEventListener('input', () => {
const newVal = getAmountValue();
valueSpan.textContent = calculateVat(newVal);
});
}
}
function addCopyIcon(element, label) {
// Ustal element docelowy dla ikony i kontener
let targetContainer = element.parentElement;
let valueElement = element;
let isDatepicker = false;
// Jeśli element to div/span (np. datepicker wrapper), ale zawiera inputa, to chcemy inputa
if (element.tagName !== 'INPUT' && element.querySelector('input')) {
valueElement = element.querySelector('input');
// W przypadku datepickera (div), on sam jest kontenerem
targetContainer = element;
}
// Sprawdź czy to datepicker
if (targetContainer.classList.contains('o_datepicker')) {
isDatepicker = true;
// Dla datepickera wychodzimy poziom wyżej (do td), żeby nie psuć wewnętrznego layoutu (ikona kalendarza)
if (targetContainer.parentElement) {
targetContainer = targetContainer.parentElement;
}
}
// Sprawdź czy ikona już nie istnieje w kontenerze
if (targetContainer && targetContainer.querySelector('.tm-copyable-icon')) return;
const icon = document.createElement('span');
icon.className = 'tm-copyable-icon';
icon.textContent = '⧉';
icon.title = `Kliknij aby skopiować ${label}`;
icon.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
let valueToCopy = '';
if (valueElement.tagName === 'INPUT') {
valueToCopy = valueElement.value;
} else {
valueToCopy = valueElement.textContent;
}
// Czyszczenie wartości
if (label === 'Kwota') {
valueToCopy = valueToCopy.replace(/[^\d,.-]/g, '').trim();
} else {
valueToCopy = valueToCopy.trim();
}
if (valueToCopy) {
const success = await tmCopyToClipboard(valueToCopy);
if (success) {
icon.classList.add('tm-copied');
icon.title = `Skopiowano: ${valueToCopy}`;
setTimeout(() => {
icon.classList.remove('tm-copied');
icon.title = `Kliknij aby skopiować ${label}`;
}, 1200);
}
}
});
// Wstawianie ikony i stylowanie
if (targetContainer) {
// Wymuś flex na kontenerze, żeby ikona była obok
targetContainer.style.display = 'flex';
targetContainer.style.alignItems = 'center';
// Jeśli to datepicker, element datepickera (dziecko) powinien zająć resztę miejsca
if (isDatepicker) {
const datepickerDiv = targetContainer.querySelector('.o_datepicker');
if (datepickerDiv) {
datepickerDiv.style.flex = '1';
}
}
targetContainer.appendChild(icon);
} else {
// Fallback
element.appendChild(icon);
}
}
// ——— Obserwator zmian DOM ———
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
// Sprawdź czy dodano modal
if (node.classList.contains('modal') || node.classList.contains('o_technical_modal') || node.querySelector('.modal-dialog')) {
// Poczekaj chwilę na renderowanie zawartości
setTimeout(() => processModal(node), 500);
}
// Czasami modal jest już w DOM, ale zmienia się jego zawartość
const modal = node.closest('.modal');
if (modal) {
setTimeout(() => processModal(modal), 500);
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();

301
idea/idea-purchase.user.js Normal file
View file

@ -0,0 +1,301 @@
// ==UserScript==
// @name IDEAERP - Zamówienia Zakupu
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Kopiowanie elementów na stronach zamówień zakupu - kliknij aby skopiować
// @match https://emma.ideaerp.pl/web*
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
// @downloadURL https://n8n.emma.net.pl/webhook/purchase
// @updateURL https://n8n.emma.net.pl/webhook/purchase
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// ==/UserScript==
/* ========================OPIS==============================
WERSJA 1.0 - Kopiowanie elementów zamówień zakupu
DZIAŁANIE SKRYPTU:
- Kopiowanie nazwy zamówienia - kliknij na nazwę zamówienia (h1) aby skopiować
- Kopiowanie produktów z kolumny "Produkt" - kliknij na produkt aby skopiować SKU lub pełną nazwę
- Tryb kopiowania SKU/pełna nazwa - przełączalny przycisk
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
AUTOMATYCZNE AKTUALIZACJE:
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
- Jednoklikowa aktualizacja bez konieczności ręcznego pobierania
- Zachowanie ustawień użytkownika podczas aktualizacji
- Kompatybilność z systemem n8n do zarządzania wersjami
CHANGELOG:
v1.0 (2024) - PIERWSZA WERSJA SKRYPTU ZAKUPOWEGO
- Utworzono dedykowany skrypt dla zamówień zakupu (purchase.order)
- Dodano kopiowanie nazwy zamówienia (h1) z ikoną
- Dodano kopiowanie produktów z kolumny "Produkt" z ikoną
- Dodano tryb kopiowania SKU/pełna nazwa z przyciskiem przełączania
- Dodano funkcję extractSKU() do wyodrębniania SKU z nawiasów kwadratowych
- Dodano style CSS dla elementów h1 i td z ikonami kopiowania
- Dodano funkcje makeOrderNameCopyable() i makeProductCellCopyable()
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
- Podwyższenie numeru wersji do 1.0
AUTOR: Adam Grodecki
DATA: 2024
LICENCJA: Własność EMMA SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
================================================================ */
(function() {
'use strict';
// ——— Stałe ———
const COPY_MODE_KEY = 'tm_purchase_copy_mode';
const INIT_FLAG = 'tm_purchase_init_flag';
// ——— Style CSS ———
GM_addStyle(`
/* Kopiowanie nazwy zamówienia ikona dla h1 */
h1.tm-copyable { cursor: copy; }
h1.tm-copyable::after {
content: '⧉'; /* symbol kopiowania */
display: inline-block; /* w tej samej linii co tekst */
margin-left: 8px; /* odstęp od tekstu */
font-size: 14px; /* większy rozmiar dla h1 */
line-height: 1;
color: #ff9800; /* pomarańczowy */
opacity: .75; /* widoczna także bez hovera */
vertical-align: baseline;
pointer-events: none;
transition: opacity .15s ease;
}
h1.tm-copyable:hover::after { opacity: 1; }
h1.tm-copyable.tm-copied {
outline: 2px solid #ff9800;
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
}
/* Kopiowanie produktów ikona tuż za tekstem (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 tekst */
margin-left: 6px; /* mały odstęp od tekstu */
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));
}
/* Przycisk przełączania trybu kopiowania w zakładkach */
.tm-copy-mode-btn {
background: #ff9800 !important;
color: white !important;
border: none !important;
padding: 8px 12px !important;
border-radius: 4px !important;
font-size: 12px !important;
cursor: pointer !important;
margin-left: 10px !important;
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
.tm-copy-mode-btn:hover {
background: #f57c00 !important;
}
`);
// ——— Pomocnicze ———
function isPurchaseOrderForm() {
const params = new URLSearchParams((location.hash || '').replace(/^#/, ''));
const model = params.get('model');
const viewType = params.get('view_type');
return model === 'purchase.order' && viewType === 'form';
}
async function tmCopyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
}
catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
// ——— Funkcja wyodrębniania SKU z nawiasów kwadratowych ———
function extractSKU(productText) {
const match = productText.match(/\[([^\]]+)\]/);
return match ? match[1] : productText;
}
// ——— Kopiowanie nazwy zamówienia ———
function makeOrderNameCopyable(h1Element) {
if (!h1Element || h1Element.dataset.tmOrderNameCopyable) return;
h1Element.dataset.tmOrderNameCopyable = '1';
h1Element.classList.add('tm-copyable');
h1Element.style.cursor = 'pointer';
h1Element.title = 'Kliknij aby skopiować nazwę zamówienia';
h1Element.addEventListener('click', async (e) => {
e.stopPropagation();
const orderName = (h1Element.textContent || '').trim();
if (!orderName) return;
try {
await tmCopyToClipboard(orderName);
const old = h1Element.title;
h1Element.title = `Skopiowano nazwę zamówienia: ${orderName}`;
h1Element.classList.add('tm-copied');
setTimeout(() => {
h1Element.title = old || 'Kliknij aby skopiować nazwę zamówienia';
h1Element.classList.remove('tm-copied');
}, 1200);
} catch (error) {
console.error('[Tampermonkey] Błąd podczas kopiowania nazwy zamówienia:', error);
h1Element.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Kopiowanie produktów z trybem SKU/pełna nazwa ———
function makeProductCellCopyable(td) {
if (!td || td.dataset.tmProductCopyable) return;
td.dataset.tmProductCopyable = '1';
td.classList.add('tm-copyable');
td.addEventListener('click', async (e) => {
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
const fullText = (td.textContent || '').trim();
if (!fullText) return;
try {
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true); // domyślnie SKU
const textToCopy = copyOnlySKU ? extractSKU(fullText) : fullText;
await tmCopyToClipboard(textToCopy);
const old = td.title;
const mode = copyOnlySKU ? 'SKU' : 'pełną nazwę';
td.title = `Skopiowano ${mode}: ${textToCopy}`;
td.classList.add('tm-copied');
setTimeout(() => { td.title = old || 'Skopiowano'; td.classList.remove('tm-copied'); }, 1200);
} catch (error) {
console.error('[Tampermonkey] Błąd podczas kopiowania produktu:', error);
td.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
// ——— Lokalizowanie docelowego miejsca dla przycisku ———
function findActionsContainer() {
// Znajdź kontener przycisków poniżej zakładek
const massBtn = document.querySelector('button[name="523"]');
if (massBtn) return massBtn.closest('.tab-pane') || massBtn.parentElement;
// Fallback - znajdź kontener o_form_buttons
const buttonsContainer = document.querySelector('.o_form_buttons');
if (buttonsContainer) return buttonsContainer;
return null;
}
// ——— Dodaj przycisk przełączania trybu kopiowania ———
function addCopyModeToggleButton() {
const host = findActionsContainer();
if (!host) {
console.log('[Tampermonkey] Nie znaleziono kontenera akcji, pomijam dodawanie przycisku');
return;
}
const idBtn = 'tm-purchase-copy-mode-toggle-button';
if (host.querySelector('#' + idBtn)) {
console.log('[Tampermonkey] Przycisk już istnieje, pomijam');
return;
}
console.log('[Tampermonkey] Znaleziono kontener akcji:', host);
console.log('[Tampermonkey] Tworzę przycisk przełączania trybu kopiowania');
const btn = document.createElement('button');
btn.type = 'button';
btn.id = idBtn;
btn.className = 'btn btn-secondary';
const label = document.createElement('span');
btn.appendChild(label);
const refresh = () => {
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true);
label.textContent = copyOnlySKU ? 'Kopiuję: SKU' : 'Kopiuję: Pełna nazwa';
btn.title = copyOnlySKU ? 'Kliknij aby przełączyć na kopiowanie pełnej nazwy produktu' : 'Kliknij aby przełączyć na kopiowanie tylko SKU produktu';
console.log('[Tampermonkey] Tryb kopiowania:', copyOnlySKU ? 'SKU' : 'Pełna nazwa');
};
btn.addEventListener('click', () => {
const currentMode = GM_getValue(COPY_MODE_KEY, true);
GM_setValue(COPY_MODE_KEY, !currentMode);
refresh();
console.log('[Tampermonkey] Tryb kopiowania zmieniony na:', !currentMode ? 'SKU' : 'Pełna nazwa');
});
// Dodaj przycisk do kontenera przycisków (zawsze poniżej zakładek)
const massBtn = host.querySelector('button[name="523"]');
if (massBtn && massBtn.parentElement) {
massBtn.insertAdjacentElement('afterend', btn);
console.log('[Tampermonkey] Przycisk dodany obok przycisku Masowa zmiana');
} else {
host.appendChild(btn);
console.log('[Tampermonkey] Przycisk dodany do kontenera akcji');
}
refresh();
}
// ——— Główna procedura ———
function processOnce() {
if (!isPurchaseOrderForm()) return;
if (!GM_getValue(INIT_FLAG)) { GM_setValue(INIT_FLAG, true); }
// Dodaj kopiowanie nazwy zamówienia
const orderNameSpan = document.querySelector('h1 span[name="name"]');
if (orderNameSpan) {
const orderNameH1 = orderNameSpan.closest('h1');
if (orderNameH1) {
makeOrderNameCopyable(orderNameH1);
}
}
// Dodaj kopiowanie produktów z kolumny "Produkt"
// Komórki produktów mają klasę o_list_many2one i są w 3. kolumnie tabeli
const productCells = document.querySelectorAll('td.o_data_cell.o_field_cell.o_list_many2one.o_readonly_modifier.o_required_modifier');
productCells.forEach(cell => {
// Sprawdź czy to rzeczywiście komórka produktu (zawiera nawiasy kwadratowe z SKU)
const text = cell.textContent || '';
if (text.includes('[') && text.includes(']')) {
makeProductCellCopyable(cell);
}
});
// Dodaj przycisk przełączania trybu kopiowania
addCopyModeToggleButton();
}
// ——— Start + reagowanie na zmiany ———
let deb = null; const debounce = (fn, ms = 150) => { clearTimeout(deb); deb = setTimeout(fn, ms); };
window.addEventListener('load', () => debounce(processOnce, 150));
window.addEventListener('hashchange', () => debounce(processOnce, 250));
new MutationObserver(() => debounce(processOnce, 150)).observe(document.documentElement, { childList: true, subtree: true });
})();

1023
idea/kfz-listing.html Normal file

File diff suppressed because it is too large Load diff

2254
idea/kfz.html Normal file

File diff suppressed because one or more lines are too long

2725
idea/orders.html Normal file

File diff suppressed because one or more lines are too long

0
idea/purchase.html Normal file
View file