commit 0515809c53a70d27b90f06f5576477aad18842b6 Author: Adam Grodecki Date: Mon Dec 15 19:42:21 2025 +0100 Initial commit diff --git a/.rules b/.rules new file mode 100644 index 0000000..fd2f5c0 --- /dev/null +++ b/.rules @@ -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" diff --git a/allegro/allegro-stat.user.js b/allegro/allegro-stat.user.js new file mode 100644 index 0000000..0313b37 --- /dev/null +++ b/allegro/allegro-stat.user.js @@ -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(); + } + +})(); diff --git a/idea/idea-edi-tim.user.js b/idea/idea-edi-tim.user.js new file mode 100644 index 0000000..d993172 --- /dev/null +++ b/idea/idea-edi-tim.user.js @@ -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 = ` +
+ + +
+ `; + 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); +})(); diff --git a/idea/idea-invoice-famica.user.js b/idea/idea-invoice-famica.user.js new file mode 100644 index 0000000..0287c3a --- /dev/null +++ b/idea/idea-invoice-famica.user.js @@ -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 = ' 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 = ' 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 = ' 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 + }); +})(); \ No newline at end of file diff --git a/idea/idea-kfz-listing.user.js b/idea/idea-kfz-listing.user.js new file mode 100644 index 0000000..f08c607 --- /dev/null +++ b/idea/idea-kfz-listing.user.js @@ -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 aż znikną + - 🎯 Wszystkie wywołania initEnhancements zastąpione waitForPageLoad + - 📊 Szczegółowe logi procesu oczekiwania na załadowanie + - ✅ Rozwiązano problem "Table: false, Breadcrumb: null" po odświeżeniu + + ZMIANY W WERSJI 2.6: + - 🎯 DODANO wykrywanie strony przez breadcrumb "Korekty" + - 🔄 ZMIENIONA logika: aktywacja nawet bez danych KFZ (mogą się załadować później) + - 📍 Backup timer sprawdza breadcrumb co 3 sekundy + - 🚀 Rozwiązano problem z URL: action=278&model=account.move&view_type=list&cids=1&menu_id=141 + - 📊 Dodatkowe logi dla debugowania wykrywania strony + + ZMIANY W WERSJI 2.5: + - 🔄 DODANO wielopoziomowe wykrywanie zmian URL dla aplikacji SPA + - 📍 5 metod wykrywania: hashchange, URL polling, title observer, breadcrumb observer, backup timer + - ⚡ Szybsze reakcje: 200ms, 300ms, 400ms, 500ms, 1000ms, 1500ms + - 🎯 Rozwiązano problem z brakiem działania przy nawigacji (bez odświeżania) + - 🔍 Backup sprawdzanie co 3 sekundy czy strona KFZ ma filtr + - 📊 Szczegółowe logi dla debugowania zmian URL + + ZMIANY W WERSJI 2.4: + - 🔧 PRZENIESIONO filtr za przyciski w lewym panelu (.o_cp_left) + - 🚫 ROZWIĄZANO problem nakładania się na wyszukiwarkę + - 📍 Filtr teraz w linii: [Zapisz] [Odrzuć] [🔽 Dostawca] | [Wyszukiwarka] + - 💫 Lepsze wykorzystanie przestrzeni bez konfliktów + + ZMIANY W WERSJI 2.3: + - 🔧 NAPRAWIONO pozycjonowanie filtra - teraz jest OBOK wyszukiwarki + - 📍 Filtr wstawiony do `.o_searchview_input_container` zamiast `.o_cp_searchview` + - 💫 Filtr i wyszukiwarka w tej samej linii (flex-shrink: 0) + - 🎯 Lepsze wykorzystanie przestrzeni w panelu kontrolnym + + ZMIANY W WERSJI 2.2: + - 🎨 PRZENIESIONO filtr dostawców do panelu kontrolnego (.o_cp_searchview) + - 💫 Filtr ma teraz profesjonalny wygląd zgodny z interfejsem Odoo + - 📍 Umieszczony obok wyszukiwarki z ikoną i odpowiednim stylem + - 🔄 Dodano fallback - jeśli brak panelu, filtr pojawi się przed tabelą + - 🎯 Zaktualizowano funkcję filtrowania na nowe indeksy pozycyjne + + ZMIANY W WERSJI 2.1: + - 🔧 NAPRAWIONO błąd TypeError w isKfzListingPage() + - 🎯 ZASTĄPIONO wyszukiwanie po data-name na indeksy pozycyjne + - 📍 Mapowanie kolumn na podstawie rzeczywistej struktury: + * Komórka 2: Numer faktury (KFZ/...) + * Komórka 4: Data faktury (DD.MM.YYYY) + * Komórka 5: Dokument źródłowy (ZZ/...) + * Komórka 6: Odnośnik (KFAS/...) + * Komórka 10: Należność (...zł) + * Komórka 11: Na podstawie (KFZ/... (...)) + - ✅ Wszystkie kolumny powinny teraz działać poprawnie! + + ZMIANY W WERSJI 2.0: + - Dodano bardzo szczegółowe debugowanie dla wszystkich kolumn + - Logowanie struktury pierwszego wiersza z klasami CSS + - Szczegółowe logi dla każdej poszukiwanej kolumny: + * invoice_origin (Dokument źródłowy) + * ref (Odnośnik) + * amount_residual_signed (Należność) + * reversed_entry_id (Na podstawie) + - Informowanie o pustych wartościach i myślnikach + - Wyraźne oznaczenie ❌ gdy kolumna nie została znaleziona + + ZMIANY W WERSJI 1.9: + - Usunięto niepotrzebne kopiowanie z kolumny NETTO (amount_untaxed_signed) + - Dodano sprawdzanie data-name żeby unikać kopiowania z NETTO + - Pozostawiono tylko kopiowanie z kolumny NALEŻNOŚĆ (amount_residual_signed) + - Ograniczono debugowanie do pierwszego wiersza + - Dodano szczegółowe logowanie dla każdej kolumny + + ZMIANY W WERSJI 1.8: + - Dodano szczegółowe debugowanie struktury tabeli + - Fallback dla różnych formatów data-name (małe/duże litery) + - Alternatywne wyszukiwanie po zawartości komórek + - Debugowanie wszystkich komórek w wierszach + - Logowanie procesu dodawania kopiowania + - Sprawdzanie czy w ogóle są wiersze danych + + ZMIANY W WERSJI 1.7: + - Zmieniono z indeksów kolumn na querySelector z data-name + - Numer: td[data-name="name"] + - Data faktury: td[data-name="invoice_date"] + - Odnośnik: td[data-name="invoice_origin"] + - Należność: td[data-name="amount_residual_signed"] + - Na podstawie: td[data-name="reversed_entry_id"] + - Dodano szczegółowe logowanie dla każdej kolumny + - Rozwiązano problem z ukrytymi kolumnami + + ZMIANY W WERSJI 1.6: + - Zmieniono nazwę skryptu na "IDEA KFZ Listing" + - Poprawiono mapowanie kolumny "Należność" (amount_residual_signed, indeks 9) + - Naprawiono kopiowanie kolumny "Odnośnik" (invoice_origin, indeks 5) + - Dodano debugowanie dla kolumny "Odnośnik" + - Wykluczenie pustych wartości i myślników "-" z kopiowania + + ZMIANY W WERSJI 1.5: + - Rozszerzone kopiowanie na 5 kolumn (Numer, Data faktury, Odnośnik, Należność, Na podstawie) + - Ulepszone czyszczenie kwot (usuwanie "zł" i znaków niełamliwych spacji) + - Automatyczne wykrywanie kolumny "Na podstawie" przez data-name="reversed_entry_id" + - Inteligentne wykrywanie dat w formacie DD.MM.YYYY + - Kopiowanie odnośników (dokumentów źródłowych) + + ZMIANY W WERSJI 1.4: + - Dodano zabezpieczenie przed duplikowaniem elementów + - Sprawdzanie czy skrypt już został uruchomiony (.kfz-script-initialized) + - Unikalne ID dla stylów CSS (#kfz-copy-styles, #kfz-table-styles) + - Sprawdzanie istnienia filtrów (.kfz-filter-container) + - Eliminacja powielania elementów przy wielokrotnym uruchomieniu + + ZMIANY W WERSJI 1.3: + - Obsługa dynamicznego ładowania danych przez AJAX + - Przechwytywanie żądań do /web/dataset/search_read + - Wykrywanie różnicy między widokiem grupowanym a szczegółowym + - Uruchamianie skryptu po kliknięciu na dostawcę + - Inteligentny MutationObserver dla nowych wierszy danych + + ZMIANY W WERSJI 1.2: + - Poprawione wykrywanie strony KFZ (sprawdza URL account.move) + - Dodano obserwację zmian DOM dla dynamicznych stron + - Lepsze debugowanie z logami w konsoli + - Obsługa zmian URL (hashchange) + - MutationObserver dla automatycznego uruchamiania + + ZMIANY W WERSJI 1.1: + - Dodano funkcjonalność kopiowania w stylu idea-orders.user.js + - Ikony kopiowania ⧉ po prawej stronie numerów KFZ (pomarańczowe) + - Kliknięcie na komórkę z numerem kopiuje do schowka + - Efekt wizualny po skopiowaniu (pomarańczowa ramka) + - Fallback dla starszych przeglądarek + - Dodano @updateURL i @downloadURL zgodnie z zasadami + + WERSJA 1.0 - Podstawowe ulepszenia listy KFZ + - Wykrywanie strony z listą faktur KFZ + - Ulepszenia wizualne tabeli + - Szybkie filtry według dostawców + - System logowania z flagą DEBUG +============================================================ */ + +(() => { + 'use strict'; + + // [Tampermonkey] Konfiguracja + const DEBUG = true; + const SCRIPT_NAME = '[IDEA KFZ Listing]'; + + /** + * Funkcja logowania z prefiksem + * @param {string} message - Wiadomość do zalogowania + */ + function log(message) { + if (DEBUG) { + console.log(`${SCRIPT_NAME} ${message}`); + } + } + + /** + * Sprawdza czy jesteśmy na stronie z listą faktur KFZ + * @returns {boolean} + */ + function isKfzListingPage() { + // Sprawdź URL - czy to model account.move + const url = window.location.href; + const isAccountMoveList = url.includes('model=account.move') && url.includes('view_type=list'); + const hasAction278 = url.includes('action=278'); + const hasMenu141 = url.includes('menu_id=141'); + + // Sprawdź czy jest tabela + const table = document.querySelector('.o_list_table'); + + // Sprawdź czy są numery KFZ w różnych formatach + const hasKfzNumbers = document.querySelector('td[title*="KFZ/"]') || + Array.from(document.querySelectorAll('td')).some(td => td.textContent.includes('KFZ/')) || + (document.textContent && document.textContent.includes('KFZ/')); + + // Sprawdź czy są wiersze danych (nie tylko grupowanie) + const hasDataRows = document.querySelectorAll('.o_data_row').length > 0; + + // Sprawdź czy to nie jest widok grupowany (ma grupy ale nie ma szczegółów) + const hasGroups = document.querySelectorAll('.o_group_header').length > 0; + const isGroupedView = hasGroups && !hasDataRows; + + // Sprawdź breadcrumb - czy zawiera "Korekty" + const breadcrumb = document.querySelector('.breadcrumb'); + const hasCorrectionsBreadcrumb = breadcrumb && breadcrumb.textContent.includes('Korekty'); + + log(`URL: ${isAccountMoveList}, Action278: ${hasAction278}, Menu141: ${hasMenu141}, Table: ${!!table}, KFZ: ${!!hasKfzNumbers}, DataRows: ${hasDataRows}, Breadcrumb: ${hasCorrectionsBreadcrumb}`); + + // Bardziej liberalne warunki - działaj jeśli: + // 1. Jest action=278 + menu_id=141 (specificzny URL dla KFZ) + // 2. LUB breadcrumb zawiera "Korekty" + // 3. LUB znaleziono numery KFZ w treści strony + const shouldActivate = (hasAction278 && hasMenu141) || + hasCorrectionsBreadcrumb || + (isAccountMoveList && hasKfzNumbers); + + log(`Czy aktywować: ${shouldActivate}`); + + return shouldActivate; + } + + /** + * Czyści duplikaty elementów z poprzednich uruchomień + */ + function cleanupDuplicates() { + // Usuń duplikaty filtrów + const filterContainers = document.querySelectorAll('.kfz-filter-container'); + if (filterContainers.length > 1) { + for (let i = 1; i < filterContainers.length; i++) { + filterContainers[i].remove(); + } + log(`Usunięto ${filterContainers.length - 1} duplikatów filtrów`); + } + + // Usuń duplikaty stylów + const copyStyles = document.querySelectorAll('#kfz-copy-styles'); + if (copyStyles.length > 1) { + for (let i = 1; i < copyStyles.length; i++) { + copyStyles[i].remove(); + } + log(`Usunięto ${copyStyles.length - 1} duplikatów stylów kopiowania`); + } + + const tableStyles = document.querySelectorAll('#kfz-table-styles'); + if (tableStyles.length > 1) { + for (let i = 1; i < tableStyles.length; i++) { + tableStyles[i].remove(); + } + log(`Usunięto ${tableStyles.length - 1} duplikatów stylów tabeli`); + } + } + + + /** + * Główna funkcja inicjalizująca ulepszenia + */ + async function initEnhancements() { + try { + log(`🔍 INIT ENHANCEMENTS - sprawdzam stronę: ${window.location.href}`); + + if (!isKfzListingPage()) { + log('❌ Nie wykryto strony z listą KFZ'); + return; + } + + log('✅ WYKRYTO STRONĘ KFZ - kontynuuję...'); + + // Sprawdź czy skrypt już został uruchomiony na tej stronie + const scriptMarker = document.querySelector('.kfz-script-initialized'); + if (scriptMarker) { + log('Skrypt już został uruchomiony - sprawdzam czy elementy istnieją'); + + // Sprawdź czy elementy rzeczywiście istnieją + const hasFilter = document.querySelector('.kfz-filter-container'); + const hasStyles = document.querySelector('#kfz-copy-styles'); + + if (!hasFilter || !hasStyles) { + log('Elementy skryptu nie istnieją mimo markera - resetuję i uruchamiam ponownie'); + scriptMarker.remove(); + // Usuń wszystkie elementy skryptu + document.querySelectorAll('.kfz-filter-container, #kfz-copy-styles, #kfz-table-styles').forEach(el => el.remove()); + } else { + log('Elementy skryptu istnieją - pomijam'); + return; + } + } + + // Usuń ewentualne duplikaty z poprzednich uruchomień + cleanupDuplicates(); + + log('Wykryto stronę z listą faktur KFZ'); + + // Oznacz że skrypt został uruchomiony + const marker = document.createElement('div'); + marker.className = 'kfz-script-initialized'; + marker.style.display = 'none'; + document.body.appendChild(marker); + + // Tutaj można dodać konkretne ulepszenia + enhanceTableVisibility(); + addQuickFilters(); + addCopyStyles(); + addCopyFunctionality(); + + log('Wszystkie ulepszenia zostały zastosowane'); + + } catch (error) { + console.error(`${SCRIPT_NAME} Błąd:`, error); + } + } + + /** + * Poprawia widoczność tabeli + */ + function enhanceTableVisibility() { + const table = document.querySelector('.o_list_table'); + if (!table) return; + + // Sprawdź czy style już zostały dodane + if (document.querySelector('#kfz-table-styles')) { + log('Style tabeli już istnieją - pomijam dodawanie'); + return; + } + + // Dodaj style CSS dla lepszej czytelności + const style = document.createElement('style'); + style.id = 'kfz-table-styles'; + style.textContent = ` + .o_list_table .text-danger { + background-color: #fff5f5 !important; + border-left: 3px solid #dc3545 !important; + } + + .o_list_table .o_data_row:hover { + background-color: #f8f9fa !important; + } + + .o_list_table th { + position: sticky; + top: 0; + background-color: #fff !important; + z-index: 10; + } + `; + document.head.appendChild(style); + + log('Zastosowano ulepszenia wizualne tabeli'); + } + + /** + * Dodaje szybkie filtry do panelu kontrolnego + */ + function addQuickFilters() { + // Sprawdź czy filtry już istnieją + if (document.querySelector('.kfz-filter-container')) { + log('Filtry już istnieją - pomijam dodawanie'); + return; + } + + // Znajdź lewą część panelu kontrolnego (za przyciskami) + const leftPanel = document.querySelector('.o_cp_left .o_cp_buttons'); + + if (!leftPanel) { + log('Nie znaleziono lewego panelu - dodaję filtr przed tabelą'); + addQuickFiltersBeforeTable(); + return; + } + + // Pobierz unikalnych dostawców z wierszy danych + const suppliers = new Set(); + const rows = document.querySelectorAll('.o_data_row'); + + rows.forEach(row => { + const supplierCell = row.children[3]; // Dostawca jest w komórce 3 + if (supplierCell) { + const supplierName = supplierCell.textContent.trim(); + if (supplierName) { + suppliers.add(supplierName); + } + } + }); + + if (suppliers.size === 0) { + log('Brak dostawców do filtrowania'); + return; + } + + // Utwórz kontener filtra - będzie za przyciskami w lewym panelu + const filterContainer = document.createElement('div'); + filterContainer.className = 'kfz-filter-container'; + filterContainer.style.cssText = ` + display: flex; + align-items: center; + gap: 8px; + margin-left: 15px; + height: 32px; + `; + + // Etykieta z ikoną + const label = document.createElement('span'); + label.innerHTML = ' Dostawca:'; + label.style.cssText = ` + color: #6c757d; + font-size: 13px; + white-space: nowrap; + font-weight: normal; + `; + + // Select z dostawcami - styl podobny do innych kontrolek Odoo + const supplierFilter = document.createElement('select'); + supplierFilter.className = 'form-control'; + supplierFilter.style.cssText = ` + height: 32px; + padding: 4px 8px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 13px; + background-color: white; + min-width: 200px; + max-width: 250px; + line-height: 1.5; + `; + + // Opcja "Wszyscy" + const allOption = document.createElement('option'); + allOption.value = ''; + allOption.textContent = 'Wszyscy dostawcy'; + supplierFilter.appendChild(allOption); + + // Opcje dla każdego dostawcy + Array.from(suppliers).sort().forEach(supplier => { + const option = document.createElement('option'); + option.value = supplier; + option.textContent = supplier; + supplierFilter.appendChild(option); + }); + + // Event listener dla filtra dostawców + supplierFilter.addEventListener('change', () => { + filterBySupplier(supplierFilter.value); + }); + + filterContainer.appendChild(label); + filterContainer.appendChild(supplierFilter); + + // Wstaw filtr za przyciskami w lewym panelu + leftPanel.parentNode.appendChild(filterContainer); + + log('Dodano filtr dostawców za przyciskami w lewym panelu'); + } + + /** + * Fallback - dodaje filtry przed tabelą jeśli nie ma panelu wyszukiwania + */ + function addQuickFiltersBeforeTable() { + const table = document.querySelector('.o_list_table'); + if (!table) return; + + // Kontener dla filtrów (stary styl) + const filterContainer = document.createElement('div'); + filterContainer.className = 'kfz-filter-container'; + filterContainer.style.cssText = ` + margin: 10px 0; + padding: 10px; + background: #f8f9fa; + border-radius: 5px; + display: flex; + gap: 10px; + align-items: center; + `; + + // Filtr po dostawcy + const supplierFilter = document.createElement('select'); + supplierFilter.style.cssText = 'padding: 5px; border-radius: 3px; border: 1px solid #ccc;'; + supplierFilter.innerHTML = ''; + + // Pobierz unikalnych dostawców + const suppliers = new Set(); + const rows = document.querySelectorAll('.o_data_row'); + + rows.forEach(row => { + const supplierCell = row.children[3]; + if (supplierCell) { + const supplierName = supplierCell.textContent.trim(); + if (supplierName) { + suppliers.add(supplierName); + } + } + }); + + suppliers.forEach(supplier => { + const option = document.createElement('option'); + option.value = supplier; + option.textContent = supplier; + supplierFilter.appendChild(option); + }); + + // Event listener dla filtra dostawców + supplierFilter.addEventListener('change', () => { + filterBySupplier(supplierFilter.value); + }); + + filterContainer.appendChild(document.createTextNode('Filtruj: ')); + filterContainer.appendChild(supplierFilter); + + // Wstaw filtry przed tabelą + table.parentNode.insertBefore(filterContainer, table); + + log('Dodano filtry przed tabelą (fallback)'); + } + + /** + * Filtruje wiersze według dostawcy + * @param {string} supplier - Nazwa dostawcy + */ + function filterBySupplier(supplier) { + const rows = document.querySelectorAll('.o_data_row'); + + rows.forEach(row => { + const supplierCell = row.children[3]; // Dostawca jest w komórce 3 + if (!supplierCell) return; + + const supplierName = supplierCell.textContent.trim(); + const shouldShow = !supplier || supplierName === supplier; + row.style.display = shouldShow ? '' : 'none'; + }); + + log(`Filtrowanie według dostawcy: ${supplier || 'wszyscy'}`); + } + + /** + * Kopiuje tekst do schowka (jak w idea-orders.user.js) + * @param {string} text - Tekst do skopiowania + */ + async function tmCopyToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + } catch (error) { + // Fallback dla starszych przeglądarek + const textArea = document.createElement('textarea'); + textArea.value = text; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + } + } + + /** + * Czyni komórkę kopiowalną (styl z idea-orders.user.js) + * @param {Element} td - Komórka tabeli + */ + function makeCellCopyable(td) { + if (!td || td.dataset.tmCopyable) return; + td.dataset.tmCopyable = '1'; + td.classList.add('tm-copyable'); + td.addEventListener('click', async (e) => { + e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu + let val = (td.textContent || '').trim(); + if (!val) return; + + // Dla kwot usuń "zł" i zbędne spacje oraz znaki specjalne + if (val.includes('zł')) { + val = val.replace(/\s*zł\s*$/, '').trim(); + // Usuń też znaki niełamliwych spacji i inne znaki formatujące + val = val.replace(/[\u00A0\u202F\u2009]/g, ' ').trim(); + } + + try { + await tmCopyToClipboard(val); + const old = td.title; + td.title = `Skopiowano: ${val}`; + td.classList.add('tm-copied'); + setTimeout(() => { + td.title = old || 'Skopiowano'; + td.classList.remove('tm-copied'); + }, 1200); + log(`Skopiowano: ${val}`); + } catch (error) { + console.error('[Tampermonkey] Błąd podczas kopiowania komórki:', error); + td.title = 'Błąd podczas kopiowania'; + } + }, true); // capture=true + } + + /** + * Dodaje funkcjonalność kopiowania do wybranych kolumn + */ + function addCopyFunctionality() { + // Znajdź wszystkie wiersze danych + const dataRows = document.querySelectorAll('.o_data_row'); + log(`Znaleziono ${dataRows.length} wierszy danych`); + + if (dataRows.length === 0) { + log('Brak wierszy danych - sprawdzam strukturę tabeli'); + const allRows = document.querySelectorAll('tr'); + log(`Wszystkich wierszy: ${allRows.length}`); + allRows.forEach((row, index) => { + if (index < 5) { // Pokaż tylko pierwsze 5 + log(`Wiersz ${index}: klasy="${row.className}", komórek=${row.children.length}`); + } + }); + } + + dataRows.forEach((row, rowIndex) => { + log(`Przetwarzam wiersz ${rowIndex}`); + + // Debuguj wszystkie komórki w wierszu (tylko pierwszy wiersz żeby nie spamować) + if (rowIndex === 0) { + log(`=== DEBUGOWANIE STRUKTURY PIERWSZEGO WIERSZA ===`); + Array.from(row.children).forEach((cell, cellIndex) => { + const dataName = cell.getAttribute('data-name'); + const text = cell.textContent.trim(); + const classes = cell.className; + log(` Komórka ${cellIndex}: data-name="${dataName}", klasy="${classes}", tekst="${text.substring(0, 40)}"`); + }); + log(`=== KONIEC DEBUGOWANIA STRUKTURY ===`); + } + + // Numer faktury - komórka 2 (na podstawie debugowania) + let numberCell = row.children[2]; + + if (numberCell && numberCell.textContent.includes('KFZ/')) { + makeCellCopyable(numberCell); + log(`✓ Dodano kopiowanie dla numeru: "${numberCell.textContent.trim()}"`); + } else if (numberCell) { + log(`Pominięto kopiowanie numeru: "${numberCell.textContent.trim()}" (brak KFZ/)`); + } else { + log(`Nie znaleziono komórki numeru (indeks 2)`); + } + + // Data faktury - komórka 4 (na podstawie debugowania) + let dateCell = row.children[4]; + + if (dateCell && dateCell.textContent.match(/\d{2}\.\d{2}\.\d{4}/)) { + makeCellCopyable(dateCell); + log(`✓ Dodano kopiowanie dla daty: "${dateCell.textContent.trim()}"`); + } else if (dateCell) { + log(`Pominięto kopiowanie daty: "${dateCell.textContent.trim()}" (nie pasuje format)`); + } else { + log(`Nie znaleziono komórki daty (indeks 4)`); + } + + // Dokument źródłowy - komórka 5 (na podstawie debugowania: "ZZ/00310") + let invoiceOriginCell = row.children[5]; + + if (invoiceOriginCell) { + const invoiceOriginText = invoiceOriginCell.textContent.trim(); + log(`Znaleziono dokument źródłowy: "${invoiceOriginText}"`); + if (invoiceOriginText && invoiceOriginText !== '' && invoiceOriginText !== '-') { + makeCellCopyable(invoiceOriginCell); + log(`✓ Dodano kopiowanie dla dokumentu źródłowego: "${invoiceOriginText}"`); + } else { + log(`Dokument źródłowy pusty lub myślnik: "${invoiceOriginText}"`); + } + } else { + log(`❌ Nie znaleziono komórki dokumentu źródłowego (indeks 5)`); + } + + // Odnośnik - komórka 6 (na podstawie debugowania: "KFAS/2025/10/269/PL") + let refCell = row.children[6]; + + if (refCell) { + const refText = refCell.textContent.trim(); + log(`Znaleziono odnośnik: "${refText}"`); + if (refText && refText !== '' && refText !== '-') { + makeCellCopyable(refCell); + log(`✓ Dodano kopiowanie dla odnośnika: "${refText}"`); + } else { + log(`Odnośnik pusty lub myślnik: "${refText}"`); + } + } else { + log(`❌ Nie znaleziono komórki odnośnika (indeks 6)`); + } + + // Należność - komórka 10 (na podstawie debugowania: "1 123,80 zł") + let amountCell = row.children[10]; + + if (amountCell) { + const amountText = amountCell.textContent.trim(); + log(`Znaleziono należność: "${amountText}"`); + + if (amountText.includes('zł')) { + makeCellCopyable(amountCell); + log(`✓ Dodano kopiowanie dla należności: "${amountText}"`); + } else { + log(`Należność bez 'zł': "${amountText}"`); + } + } else { + log(`❌ Nie znaleziono komórki należności (indeks 10)`); + } + + // Na podstawie - komórka 11 (na podstawie debugowania: "KFZ/283/09/2025 (KFAS/2025/09/799/PL)") + let basisCell = row.children[11]; + + if (basisCell) { + const basisText = basisCell.textContent.trim(); + log(`Znaleziono "Na podstawie": "${basisText}"`); + if (basisText && basisText !== '' && basisText !== '-') { + makeCellCopyable(basisCell); + log(`✓ Dodano kopiowanie dla "Na podstawie": "${basisText}"`); + } else { + log(`Na podstawie puste lub myślnik: "${basisText}"`); + } + } else { + log(`❌ Nie znaleziono komórki "Na podstawie" (indeks 11)`); + } + }); + + log('Zakończono dodawanie funkcjonalności kopiowania'); + } + + /** + * Dodaje style CSS dla funkcjonalności kopiowania (dokładnie jak w idea-orders.user.js) + */ + function addCopyStyles() { + // Sprawdź czy style już zostały dodane + if (document.querySelector('#kfz-copy-styles')) { + log('Style kopiowania już istnieją - pomijam dodawanie'); + return; + } + + const style = document.createElement('style'); + style.id = 'kfz-copy-styles'; + style.textContent = ` + /* Kopiowanie – ikona tuż za liczbą (inline), mniejsza, pomarańczowa */ + td.tm-copyable { cursor: copy; } + td.tm-copyable::after { + content: '⧉'; /* symbol kopiowania w kolorze CSS */ + display: inline-block; /* w tej samej linii co liczba */ + margin-left: 6px; /* mały odstęp od liczby */ + font-size: 12px; /* mniejszy rozmiar */ + line-height: 1; + color: #ff9800; /* pomarańczowy */ + opacity: .75; /* widoczna także bez hovera */ + vertical-align: baseline; + pointer-events: none; + transition: opacity .15s ease; + } + td.tm-copyable:hover::after { opacity: 1; } + td.tm-copyable.tm-copied { + outline: 2px solid #ff9800; + background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0)); + } + `; + document.head.appendChild(style); + } + + /** + * Czeka na załadowanie elementu + * @param {string} selector - Selektor CSS + * @param {number} timeout - Timeout w ms + * @returns {Promise} + */ + function waitForElement(selector, timeout = 5000) { + return new Promise((resolve, reject) => { + const element = document.querySelector(selector); + if (element) { + resolve(element); + return; + } + + const observer = new MutationObserver(() => { + const element = document.querySelector(selector); + if (element) { + observer.disconnect(); + resolve(element); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + setTimeout(() => { + observer.disconnect(); + reject(new Error(`Element ${selector} nie został znaleziony w czasie ${timeout}ms`)); + }, timeout); + }); + } + + // Przechwytywanie żądań AJAX do search_read + function interceptAjaxRequests() { + const originalFetch = window.fetch; + window.fetch = function(...args) { + const promise = originalFetch.apply(this, args); + + // Sprawdź czy to żądanie do search_read + if (args[0] && args[0].includes('/web/dataset/search_read')) { + promise.then(response => { + if (response.ok) { + log('Wykryto żądanie AJAX search_read - uruchamiam skrypt po opóźnieniu'); + setTimeout(initEnhancements, 1500); // Większe opóźnienie dla AJAX + } + }); + } + + return promise; + }; + + // Przechwytywanie XMLHttpRequest (backup) + const originalXHR = window.XMLHttpRequest.prototype.open; + window.XMLHttpRequest.prototype.open = function(method, url, ...args) { + if (url && url.includes('/web/dataset/search_read')) { + this.addEventListener('load', () => { + log('Wykryto XHR search_read - uruchamiam skrypt po opóźnieniu'); + setTimeout(initEnhancements, 1500); + }); + } + return originalXHR.call(this, method, url, ...args); + }; + } + + // Inicjalizacja skryptu z obserwacją zmian + function startScript() { + log('🚀 SKRYPT ZAŁADOWANY - WERSJA 3.0'); + log(`📍 URL: ${window.location.href}`); + + // Przechwytuj żądania AJAX + interceptAjaxRequests(); + + // Pierwsze uruchomienie + setTimeout(initEnhancements, 1000); + + // Obserwuj zmiany w DOM (dla dynamicznych stron) + const observer = new MutationObserver((mutations) => { + // Sprawdź czy dodano nowe wiersze danych + const hasNewDataRows = mutations.some(mutation => + Array.from(mutation.addedNodes).some(node => + node.nodeType === 1 && ( + node.classList?.contains('o_data_row') || + node.querySelector?.('.o_data_row') + ) + ) + ); + + if (hasNewDataRows) { + log('Wykryto nowe wiersze danych - uruchamiam skrypt'); + setTimeout(initEnhancements, 800); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + // Obserwuj zmiany URL (hash) - kilka metod dla pewności + let lastUrl = window.location.href; + + // 1. Standardowy hashchange + window.addEventListener('hashchange', () => { + log('Wykryto hashchange - resetuję stan i uruchamiam skrypt'); + // Resetuj stan przy zmianie URL + const scriptMarker = document.querySelector('.kfz-script-initialized'); + if (scriptMarker) { + scriptMarker.remove(); + log('Usunięto marker inicjalizacji przy hashchange'); + } + setTimeout(initEnhancements, 500); + setTimeout(initEnhancements, 1500); // Backup po dłuższym czasie + }); + + // 2. Obserwuj zmiany URL przez polling + setInterval(() => { + const currentUrl = window.location.href; + if (currentUrl !== lastUrl) { + log(`URL zmieniony z ${lastUrl} na ${currentUrl}`); + lastUrl = currentUrl; + // Resetuj stan przy zmianie URL + const scriptMarker = document.querySelector('.kfz-script-initialized'); + if (scriptMarker) { + scriptMarker.remove(); + log('Usunięto marker inicjalizacji przy zmianie URL'); + } + setTimeout(initEnhancements, 300); + setTimeout(initEnhancements, 1000); + } + }, 500); + + // 3. Obserwuj zmiany w tytule strony (Odoo często zmienia tytuł) + let lastTitle = document.title; + const titleObserver = new MutationObserver(() => { + if (document.title !== lastTitle) { + log(`Tytuł zmieniony z "${lastTitle}" na "${document.title}"`); + lastTitle = document.title; + setTimeout(initEnhancements, 400); + } + }); + + titleObserver.observe(document.querySelector('title') || document.head, { + childList: true, + characterData: true, + subtree: true + }); + + // 4. Obserwuj zmiany w breadcrumb (ścieżka nawigacji Odoo) + const breadcrumbObserver = new MutationObserver(() => { + log('Wykryto zmiany w breadcrumb - sprawdzam czy to strona KFZ'); + setTimeout(initEnhancements, 200); + }); + + // Obserwuj breadcrumb jeśli istnieje + const breadcrumbElement = document.querySelector('.breadcrumb, .o_cp_controller'); + if (breadcrumbElement) { + breadcrumbObserver.observe(breadcrumbElement, { + childList: true, + characterData: true, + subtree: true + }); + } + + // 5. Dodatkowe sprawdzenie co 3 sekundy (backup) + setInterval(() => { + const url = window.location.href; + const isAccountMoveList = url.includes('model=account.move') && url.includes('view_type=list'); + const hasFilter = document.querySelector('.kfz-filter-container'); + const breadcrumbCheck = document.querySelector('.breadcrumb'); + const hasCorrectionsBreadcrumb = breadcrumbCheck && breadcrumbCheck.textContent.includes('Korekty'); + + if (isAccountMoveList && !hasFilter && (isKfzListingPage() || hasCorrectionsBreadcrumb)) { + log('Backup check - wykryto stronę account.move bez filtra, uruchamiam skrypt'); + initEnhancements(); + } + }, 3000); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', startScript); + } else { + startScript(); + } +})(); diff --git a/idea/idea-kfz-zd.user.js b/idea/idea-kfz-zd.user.js new file mode 100644 index 0000000..df7a6f9 --- /dev/null +++ b/idea/idea-kfz-zd.user.js @@ -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, zł, 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 + - Teraz znajdujemy kolumnę po indeksie z nagłówka + - 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ę &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 są OK, ale view_type jest null - skrypt czeka + - Dodano specjalny log "⏳ Czekam na view_type=form..." gdy parametry są częściowo poprawne + - To pozwala na dłuższe czekanie aż 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 są puste + - Eliminacja problemu z ustawianiem zakresu "1-" (pusta wartość) + - Teraz poprawnie wykrywa gdy wartości są 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   i &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(/ /g, '') // Usuń   + .replace(/&nbsp;/g, '') // Usuń zakodowane &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 , 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(); + } + +})(); diff --git a/idea/idea-kfz.user.js b/idea/idea-kfz.user.js new file mode 100644 index 0000000..a1a8361 --- /dev/null +++ b/idea/idea-kfz.user.js @@ -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:
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'); +})(); diff --git a/idea/idea-order-shipping.user.js b/idea/idea-order-shipping.user.js new file mode 100644 index 0000000..6b5fd64 --- /dev/null +++ b/idea/idea-order-shipping.user.js @@ -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 }); + })(); diff --git a/idea/idea-orders.user.js b/idea/idea-orders.user.js new file mode 100755 index 0000000..50f29e5 --- /dev/null +++ b/idea/idea-orders.user.js @@ -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) + są 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 }); + })(); diff --git a/idea/idea-payment-register.user.js b/idea/idea-payment-register.user.js new file mode 100644 index 0000000..333cd0e --- /dev/null +++ b/idea/idea-payment-register.user.js @@ -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 = ` + + + + + ${calculatedValue} + + + `; + + // 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 }); + +})(); diff --git a/idea/idea-purchase.user.js b/idea/idea-purchase.user.js new file mode 100644 index 0000000..42a84b5 --- /dev/null +++ b/idea/idea-purchase.user.js @@ -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 }); + })(); diff --git a/idea/kfz-listing.html b/idea/kfz-listing.html new file mode 100644 index 0000000..8ba0652 --- /dev/null +++ b/idea/kfz-listing.html @@ -0,0 +1,1023 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
# +
+
NumerDostawcaData fakturyDokument źródłowyTermin płatnościNetto + SumaNależność + Status + +
AW-NARZĘDZIA WALENTY ANDROSIUK SPÓŁKA + KOMANDYTOWA (1)61,0075,0375,03
CSG SPÓŁKA AKCYJNA (9)1 073,491 320,411 320,41
NEWELL POLAND SERVICES SPÓŁKA Z OGRANICZONĄ + ODPOWIEDZIALNOŚCIĄ (28)-12 232,74-15 046,28-15 046,28
1 +
+ +
+
KFZ/300/10/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ02.10.2025ZZ/00515 + 31.12.2025146,11 zł179,72 zł179,72 złZaksięgowano
2 +
+
KFZ/299/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/03090 + 29.12.2025218,84 zł269,17 zł269,17 złZaksięgowano
3 +
+
KFZ/298/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/01733 + 29.12.202561,23 zł75,31 zł75,31 złZaksięgowano
4 +
+
KFZ/297/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/01616 + 29.12.202596,01 zł118,09 zł118,09 złZaksięgowano
5 +
+
KFZ/296/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/00392 + 29.12.202522,90 zł28,17 zł28,17 złZaksięgowano
6 +
+
KFZ/295/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/01503 + 29.12.202518,45 zł22,69 zł22,69 złZaksięgowano
7 +
+
KFZ/294/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/01503 + 29.12.202519,75 zł24,29 zł24,29 złZaksięgowano
8 +
+
KFZ/293/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/01300 + 29.12.2025150,00 zł184,50 zł184,50 złZaksięgowano
9 +
+
KFZ/292/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/01350 + 29.12.20258,20 zł10,09 zł10,09 złZaksięgowano
10 +
+
KFZ/289/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.202529.12.2025264,96 zł325,90 zł325,90 złZaksięgowano
11 +
+
KFZ/288/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/02591 + 29.12.202543,40 zł53,38 zł53,38 złZaksięgowano
12 +
+
KFZ/287/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ30.09.2025ZZ/00800 + 29.12.2025308,94 zł380,00 zł380,00 złZaksięgowano
13 +
+
KFZ/285/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ24.09.2025ZZ/00238 + 23.12.202510,57 zł13,00 zł13,00 złZaksięgowano
14 +
+
KFZ/284/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ24.09.2025ZZ/02518 + 23.12.2025871,00 zł1 071,33 zł1 071,33 złZaksięgowano
15 +
+
KFZ/286/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ18.09.202517.12.202517,40 zł21,40 zł21,40 złZaksięgowano
16 +
+
KFZ/278/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ18.09.2025ZZ/00800 + 17.12.2025122,28 zł150,40 zł150,40 złZaksięgowano
17 +
+
KFZ/277/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ18.09.2025ZZ/02263 + 17.12.2025100,01 zł123,01 zł123,01 złZaksięgowano
18 +
+
KFZ/276/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ17.09.2025ZZ/01350 + 16.12.20251,80 zł2,21 zł2,21 złZaksięgowano
19 +
+
KFZ/275/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ17.09.2025ZZ/01300 + 16.12.202511,95 zł14,70 zł14,70 złZaksięgowano
20 +
+
KFZ/274/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ17.09.2025ZZ/01616 + 16.12.202532,09 zł39,47 zł39,47 złZaksięgowano
21 +
+
KFZ/273/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ12.09.2025ZZ/00261 + 11.12.202523,00 zł28,29 zł28,29 złZaksięgowano
22 +
+
KFZ/272/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ12.09.202526.09.202582,38 zł101,33 zł101,33 złZaksięgowano
23 +
+
KFZ/270/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ12.09.2025ZZ/02249 + 11.12.2025276,04 zł339,53 zł339,53 złZaksięgowano
24 +
+
KFZ/269/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ12.09.202511.12.2025167,02 zł205,43 zł205,43 złZaksięgowano
25 +
+
KFZ/268/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ12.09.2025ZZ/02243 + 11.12.2025337,38 zł414,98 zł414,98 złZaksięgowano
26 +
+
KFZ/267/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ12.09.202511.12.202514,85 zł18,27 zł18,27 złZaksięgowano
27 +
+
KFZ/279/09/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ05.09.2025ZZ/01350 + 04.12.20258,10 zł9,96 zł9,96 złZaksięgowano
28 +
+
KFZ/250/08/2025NEWELL POLAND SERVICES SPÓŁKA Z + OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ29.08.202527.11.2025-15 667,40 zł-19 270,90 zł-19 270,90 złZaksięgowano
-11 098,25-13 650,84-13 650,84
\ No newline at end of file diff --git a/idea/kfz.html b/idea/kfz.html new file mode 100644 index 0000000..335ce8b --- /dev/null +++ b/idea/kfz.html @@ -0,0 +1,2254 @@ + + + + + + KFZ/306/10/2025 (17/2585110) - IDEAerp + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + + +
+Zapłacona +
+Płatność w toku +
261 855Korekta zakupuNie zapłaconepurchasePLNPSA Parts Ltd. 2 Prince George's Road Colliers Wood London, SW19 2PX UKEMMA SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
Korekta zakupu

Projekt fakturyProjekt korektyProjekt fakturyProjekt korektyProjekt fakturyProjekt fakturyy

Pierwszy numer:

KFZ/306/10/2025
1300
1

PSA Parts Ltd. 2 Prince George's Road Colliers Wood London, SW19 2PX UK
PSA Parts Ltd. 2 Prince George's Road Colliers Wood London, SW19 2PX UK
PSA Parts Ltd. 2 Prince George's Road Colliers Wood London, SW19 2PX UK
2 Prince George's Road Colliers Wood
London
SW19 2PX
Holandia
NL 823908628B01
+44 (0) 20 8685 6203
PSA Parts Ltd. 2 Prince George's Road Colliers Wood London, SW19 2PX UK
PSA Parts Ltd. 2 Prince George's Road Colliers Wood London, SW19 2PX UK
17/2585110
14.10.2025
15.04.2025
15.04.2025
13.11.2025
Faktury zakupu (PLN)
+ +
EMMA SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
PLN
01.01.1900
1,000000
17/2584897
+ + +
+ +
#ProduktEtykietaJMIlość po korekcieCena jednostkowa po korekcieUpust(%)PodatkiWartośćNettoPodatekBrutto
+ + +
1[DRC2L] Akumulator Duracell DRC2L 7,4V700mAh Li-IonZZ/00531: [DRC2L] Lithium ion Battery 7.4V 700mAhSztuki20,00038,65000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł773,000,00773,00
2[DR9618] Akumulator Duracell DR9618 3,7V650mAh Li-IonZZ/00531: [DR9618] Lithium ion Battery 3.7V 700mAhSztuki12,00029,86000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł358,320,00358,32
3[DR9963] Akumulator Duracell DR9963 3,7V700mAh Li-IonZZ/00531: [DR9963] Lithium ion Battery 3.7V 700mAhSztuki14,00029,86000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł418,040,00418,04
4[DR9900] Akumulator Duracell DR9900 7,4V1100mAh Li-IonZZ/00531: [DR9900] Lithium ion Battery 7.4V 1100mAhSztuki10,00046,29000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł462,900,00462,90
5[DR9954] Akumulator Duracell DR9954 7,4V1030mAh Li-IonZZ/00531: [DR9954] Lithium ion Battery 7.4V 1030mAhSztuki20,00049,13000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł982,600,00982,60
6[DRGOPROH4] Akumulator Duracell GoPro H4 3,8V1160mAh Li-IonZZ/00531: [DRGOPROH4] Lithium ion Battery 3.8V 1160mAhSztuki20,00030,76000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł615,200,00615,20
7[DR9688] Akumulator Duracell DR9688 3,7V950mAh Li-IonZZ/00531: [DR9688] Lithium ion Battery 3.7V 950mAhSztuki12,00031,86000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł382,320,00382,32
8[DR9967] Akumulator Duracell DR9967 7,4V1020mAh Li-IonZZ/00531: [DR9967] Lithium ion Battery 7.4V 1020mAhSztuki16,00040,69000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł651,040,00651,04
9[DR9714] Akumulator Duracell DR9714 3,6V1020mAh Li-IonZZ/00531: [DR9714] Lithium ion Battery 3.6V 1020mAhSztuki13,00033,80000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł439,400,00439,40
10[DR9700A] Akumulator Duracell DR9700A 7,4V700mAh Li-IonZZ/00531: [DR9700A] Lithium ion Battery 7.4V 700mAhSztuki10,00049,83000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł498,300,00498,30
11[DR9947] Akumulator Duracell DR9947 3,7V700mAh Li-IonZZ/00531: [DR9947] Lithium ion Battery 3.7V 700mAhSztuki28,00029,86000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł836,080,00836,08
12[DR9945] Akumulator Duracell DR9945 7,4V1020mAh Li-IonZZ/00531: [DR9945] Lithium ion Battery 7.4V 1020mAhSztuki10,00042,89000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł428,900,00428,90
13[DRSBX1] Akumulator Duracell DRSBX1 3,7V1090mAh Li-IonZZ/00531: [DRSBX1] Lithium ion Battery 3.7V 1090mAhSztuki14,00029,86000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł418,040,00418,04
14[DRNEL14] Akumulator Duracell DRNEL14 7,4V1100mAh Li-IonZZ/00531: [DRNEL14] Lithium ion Battery 7.4V 1100mAhSztuki15,00059,77000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł896,550,00896,55
15[DR9925] Akumulator Duracell DR9925 7,4V1020mAh Li-IonZZ/00531: [DR9925] Lithium ion Battery 7.4V 1020mAhSztuki10,00039,64000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł396,400,00396,40
16[DRNEL3] Akumulator Duracell DRNEL3 7,4V1600mAh Li-IonZZ/00531: [DRNEL3] Lithium ion Battery 7.4V 1600mAhSztuki8,00049,83000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
-99,66 zł398,640,00398,64
17[DR9964] Akumulator Duracell DR9964 7,4V1100mAh Li-IonZZ/00531: [DR9964] Lithium ion Battery 7.4V 1100mAhSztuki10,00043,09000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł430,900,00430,90
18[DR9641] Akumulator Duracell DR9641 3,7V1180mAh Li-IonZZ/00531: [DR9641] Lithium ion Battery 3.7V 1180mAhSztuki28,00029,86000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł836,080,00836,08
19[DR9668] Akumulator Duracell DR9668 7,4V 750mAh Li-IonZZ/00531: [DR9668] Lithium ion Battery 7.4V 750mAhSztuki20,00035,85000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł717,000,00717,00
20[DR9943] Akumulator Duracell DR9943 7,4V1600mAh Li-IonZZ/00531: [DR9943] Lithium ion Battery 7.4V 1600mAhSztuki10,00049,83000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł498,300,00498,30
21[DRC511] Akumulator Duracell DRC511 7,4V1600mAh Li-IonZZ/00531: [DRC511] Lithium ion Battery 7.4V 1600mAhSztuki20,00039,84000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł796,800,00796,80
22[SHIPP] WysyłkaZZ/00531: [SHIPP] Shipment ProductSztuki1,00045,00000,00
+ + + +
+ + + + Wewnątrzwspólnotowa dostawa towarów + + + +
+ +
0,00 zł45,000,0045,00
+ + +
#KontoEtykietaWinienMaPodatki
121-000-000 Rozrachunki z dostawcami99,66 zł0,00 zł
+ +
230-020-000 Rozliczenie zakupu towarówZZ/00531: [DRC2L] Lithium ion Battery 7.4V 700mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
330-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9618] Lithium ion Battery 3.7V 700mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
430-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9963] Lithium ion Battery 3.7V 700mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
530-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9900] Lithium ion Battery 7.4V 1100mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
630-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9954] Lithium ion Battery 7.4V 1030mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
730-020-000 Rozliczenie zakupu towarówZZ/00531: [DRGOPROH4] Lithium ion Battery 3.8V 1160mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
830-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9688] Lithium ion Battery 3.7V 950mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
930-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9967] Lithium ion Battery 7.4V 1020mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1030-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9714] Lithium ion Battery 3.6V 1020mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1130-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9700A] Lithium ion Battery 7.4V 700mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1230-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9947] Lithium ion Battery 3.7V 700mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1330-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9945] Lithium ion Battery 7.4V 1020mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1430-020-000 Rozliczenie zakupu towarówZZ/00531: [DRSBX1] Lithium ion Battery 3.7V 1090mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1530-020-000 Rozliczenie zakupu towarówZZ/00531: [DRNEL14] Lithium ion Battery 7.4V 1100mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1630-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9925] Lithium ion Battery 7.4V 1020mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1730-020-000 Rozliczenie zakupu towarówZZ/00531: [DRNEL3] Lithium ion Battery 7.4V 1600mAh0,00 zł99,66 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1830-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9964] Lithium ion Battery 7.4V 1100mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
1930-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9641] Lithium ion Battery 3.7V 1180mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
2030-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9668] Lithium ion Battery 7.4V 750mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
2130-020-000 Rozliczenie zakupu towarówZZ/00531: [DR9943] Lithium ion Battery 7.4V 1600mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
2230-020-000 Rozliczenie zakupu towarówZZ/00531: [DRC511] Lithium ion Battery 7.4V 1600mAh0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
2330-030-000 Rozliczenie zakupu usług obcychZZ/00531: [SHIPP] Shipment Product0,00 zł0,00 zł
+ + + +
+ + + + -Podstawa - Dostawa towarów/usług, kraj, 0% + + + +
+ +
99,6699,66
Faktura
Joanna Mania
Detal
ZZ/00531
KFZ/305/10/2025 (17/2585109)
+ +
+ +
false
Księgowość
false
Transakcje
Zakres
14.10.2025
2025-04-15
10.04.2025
15.04.2025
14.10.2025
Informacje o dostawie
false
Inne
Inne
11
Korekta danych teleadresowych
Synchronizacja z chmurą
EMMA SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
17/2584897
+
\ No newline at end of file diff --git a/idea/orders.html b/idea/orders.html new file mode 100644 index 0000000..bd6d442 --- /dev/null +++ b/idea/orders.html @@ -0,0 +1,2725 @@ + + + + + + Allegro00000190-3 - IDEAerp + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

Nowa integracja z przewoźnikiem Olza Logistic. Wysyłaj prościej i taniej do krajów Europy Środkowo-Wschodniej! +https://www.olzalogistic.com/landing-idea-erp

+ + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Dla zamówienia został już utworzony list przewozowy. Zmiany w zamówieniu nie wpłyną na zamówioną wysyłkę. +

Allegro00000190-3

Kontakt - Mariusz Suszczyński
Adres rozliczeniowy
Mariusz Suszczyński, Mariusz Suszczyński
Przedpłata
Mariusz Suszczyński
Leśna 36a/10
Ruda Śląska41-706
9arr0u93qc+2473eb291@allegromail.pl
Adres dostawy
Mariusz Suszczyński, MARIUSZ SUSZCZYŃSKI
MARIUSZ SUSZCZYŃSKI
Leśna 36a / 10
RUDA ŚLĄSKA41-706
+48692851705
Polska
9arr0u93qc+2473eb291@allegromail.pl
31.01.2025 11:51:36
31.01.2025 11:51:36
Allegro
sklep_Parker
marinson
39a08cc0-dfc1-11ef-b3cb-cb514d6629c7
+ +
0,00 zł
Polska, ecommerce
Cennik katalogowy PLN (PLN)
Stanowisko 1
Wysyłam z Allegro One Box, One Kurier (Allegro pl)
AL032SRS
Przy bocznej ścianie bloku, na prawo od wejścia do klatki schodowej
Przelewy24
;;ZW-AF-P24-7/02/2025-MGRO;;
PLN
+ + +
#ProduktOpisIlośćJMCena jedn. nettoCena jedn. bruttoPodatkiUpust(%)SumaNettoPodatekBruttoData zakupuOpcja
1[UNDEFINED] Niezidentyfikowany produktZestaw Długopis Parker IM Red GT w pudełku z torebką prezentową1,000
+ +
Sztuki243,81299,89
+ + + +
+ + + + VAT-23% brutto + + + +
+ +
0,00243,81 zł243,8156,08299,8931.01.2025 11:51:26
+ + +
#ProduktOpisIlośćJMCena jednostkowaUpust(%)
 
Sprzedaż
Administrator IDEAerp
Allegro - sklep_Parker
Polska
EMMA IAI- PLN PKO BP - SWIFT (BIC) BPKOPLPW 98 1020 1811 0000 0802 0365 0660
EMMA SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
https://podsumowanie.emma.ideaerp.pl/#/a09cd1ae-cc48-40bb-88cc-91201f02d32339fb72be-790f-4774-9028-6aad2a23d55b7a68479c-690a-484d-9210-cd12bc718c1e
https://salescenter.allegro.com/orders/39a08cc0-dfc1-11ef-b3cb-cb514d6629c7
0,00
Fakturowanie
+ +
Nie ma nic do fakturowania
Wydanie zewnętrzne
Magazyn Główny
Kiedy wszystkie produkty są dostępne
Spodziewana:
RMA
983439E1
Zewnętrzne faktury
+ + +
#Nazwa na fakturęInvoice File
 
Raportowanie
Allegro
Plik binarny + +
+ + +
#DataNazwaDziennikKontrahentFakturyKwotaRodzaj transakcjiStatus
107.02.2025P24/2025/8241Przelewy24 (PLN) Mariusz Suszczyński
+ +
299,89Wyślij pieniądzeZatwierdzone
231.01.2025P24/2025/6639Przelewy24 (PLN)Mariusz Suszczyński, Mariusz Suszczyński
+ +
299,89Przyjmij pieniądzeZatwierdzone
1
+ + +
#Data listu przewozowegoNazwa
 
+
\ No newline at end of file diff --git a/idea/purchase.html b/idea/purchase.html new file mode 100644 index 0000000..e69de29