Initial commit
This commit is contained in:
commit
0515809c53
97
.rules
Normal file
97
.rules
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
# ===========================
|
||||||
|
# Tampermonkey – zasady ogólne
|
||||||
|
# ===========================
|
||||||
|
- file-type: "Zawsze generuj userscript jako plik .user.js"
|
||||||
|
- header-block: |
|
||||||
|
Każdy skrypt musi zaczynać się od pełnego bloku nagłówka Tampermonkey, np.:
|
||||||
|
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Example Script
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.0
|
||||||
|
// @description Krótki opis działania skryptu
|
||||||
|
// @author Adam
|
||||||
|
// @match https://example.com/*
|
||||||
|
// @icon https://example.com/favicon.ico
|
||||||
|
// @grant none
|
||||||
|
// @run-at document-end
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
- comments: "Dodawaj komentarze do głównych sekcji skryptu"
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Styl kodu
|
||||||
|
# ===========================
|
||||||
|
- naming:
|
||||||
|
variables: "camelCase"
|
||||||
|
functions: "camelCase"
|
||||||
|
classes: "PascalCase"
|
||||||
|
- prefer-use: "const i let zamiast var"
|
||||||
|
- async: "Preferuj async/await zamiast then()"
|
||||||
|
- strict-mode: "Zawsze używaj 'use strict'; na początku"
|
||||||
|
- formatting: "Stosuj ESLint/Prettier style (2 spacje, średnik na końcu)"
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Struktura userscriptu
|
||||||
|
# ===========================
|
||||||
|
- main-entry: "Opakuj kod w IIFE (() => { ... }) aby uniknąć globalnych kolizji"
|
||||||
|
- dom-ready: "Stosuj document-end lub waitForElement zamiast ręcznych timeoutów"
|
||||||
|
- logging: "Używaj console.log tylko w trybie debug, dodając [Tampermonkey] jako prefix"
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Najlepsze praktyki
|
||||||
|
# ===========================
|
||||||
|
- selectors: "Preferuj querySelector/querySelectorAll zamiast jQuery"
|
||||||
|
- mutation-observer: "Do dynamicznych stron używaj MutationObserver zamiast setInterval"
|
||||||
|
- storage: "Używaj GM_setValue i GM_getValue do przechowywania ustawień"
|
||||||
|
- security: "Unikaj eval() i dynamicznego wykonywania kodu"
|
||||||
|
- updates: "Dodaj @updateURL i @downloadURL jeśli skrypt ma być hostowany"
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Testy i debug
|
||||||
|
# ===========================
|
||||||
|
- debug-flag: "Dodaj stałą DEBUG = true/false i używaj jej w logach"
|
||||||
|
- error-handling: "Każdy główny blok async powinien mieć try/catch"
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Wersja i opisy
|
||||||
|
# ===========================
|
||||||
|
- po każdej zmianie podnieś numer wersji, np. z 3.0 na 3.1
|
||||||
|
- każda zmiana kodu (dodanie funkcji, poprawka błędu, refaktoryzacja) MUSI kończyć się zmianą wersji
|
||||||
|
- nawet najmniejsze poprawki wymagają podwyższenia numeru wersji
|
||||||
|
- po każdej dodanej funkcjonalności opisz to w sekcji OPIS
|
||||||
|
- po każdej zmienie dodaj to, co zostało zrobione w danej wersji
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Ustawienia dodatkowe
|
||||||
|
# ===========================
|
||||||
|
- odpowiadaj zawsze w języku polskim
|
||||||
|
- przy wyszukiwaniu kolumn w tabelach zawsze używać nazwy kolumny (atrybut title, data-name lub textContent) zamiast indeksu kolumny
|
||||||
|
- indeksy kolumn mogą się zmieniać, ale nazwy pozostają stabilne
|
||||||
|
- funkcja findColumnByName powinna sprawdzać kolejno: title, data-name, textContent
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Nagłówek userscriptu - wymagane pola
|
||||||
|
# ===========================
|
||||||
|
- icon: "Zawsze dodawaj @icon z favicon strony docelowej"
|
||||||
|
- downloadURL: "Dodaj @downloadURL dla automatycznych aktualizacji"
|
||||||
|
- updateURL: "Dodaj @updateURL dla automatycznych aktualizacji"
|
||||||
|
- grants: "Dodaj odpowiednie @grant dla używanych funkcji GM_*"
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# Dokumentacja w skrypcie
|
||||||
|
# ===========================
|
||||||
|
- opis-sekcja: |
|
||||||
|
Każdy skrypt musi mieć sekcję OPIS z:
|
||||||
|
- Wymaganymi parametrami URL
|
||||||
|
- Wymaganymi kolumnami w tabeli
|
||||||
|
- Opisem działania skryptu
|
||||||
|
- Listą funkcji kopiowania/przetwarzania
|
||||||
|
- Informacjami o zapamiętywaniu ustawień
|
||||||
|
- Informacjami o automatycznych aktualizacjach
|
||||||
|
- changelog-sekcja: |
|
||||||
|
Każdy skrypt musi mieć sekcję CHANGELOG z:
|
||||||
|
- Numerem wersji i datą
|
||||||
|
- Szczegółowym opisem zmian
|
||||||
|
- Informacją o podwyższeniu numeru wersji
|
||||||
|
- Format: "vX.Y (YYYY) - NAZWA ZMIANY"
|
||||||
475
allegro/allegro-stat.user.js
Normal file
475
allegro/allegro-stat.user.js
Normal file
|
|
@ -0,0 +1,475 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Allegro Sales Center - Koszt 1 szt Calculator
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.3.0
|
||||||
|
// @description Adds a sortable "Koszt 1 szt" column to Allegro Sales Center statistics table that calculates cost per sold item
|
||||||
|
// @author Emma
|
||||||
|
// @match https://salescenter.allegro.com/ads/panel/stats/ads?marketplaceId=allegro-pl
|
||||||
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=allegro.com
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/allegro-stat
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/allegro-stat
|
||||||
|
// @grant none
|
||||||
|
// @run-at document-idle
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses Polish currency string to number
|
||||||
|
* @param {string} value - Currency string (e.g., "1842,53 zł")
|
||||||
|
* @returns {number} - Parsed number value
|
||||||
|
*/
|
||||||
|
const parseCurrency = (value) => {
|
||||||
|
if (!value || typeof value !== 'string') return 0;
|
||||||
|
const cleaned = value.replace(/\s+zł/g, '').replace(/\s+/g, '').replace(',', '.');
|
||||||
|
return parseFloat(cleaned) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses integer string to number
|
||||||
|
* @param {string} value - Integer string (e.g., "1 002")
|
||||||
|
* @returns {number} - Parsed integer value
|
||||||
|
*/
|
||||||
|
const parseInteger = (value) => {
|
||||||
|
if (!value || typeof value !== 'string') return 0;
|
||||||
|
const cleaned = value.replace(/\s+/g, '');
|
||||||
|
return parseInt(cleaned, 10) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats number to Polish currency format
|
||||||
|
* @param {number} value - Number to format
|
||||||
|
* @returns {string} - Formatted currency string
|
||||||
|
*/
|
||||||
|
const formatCurrency = (value) => {
|
||||||
|
if (isNaN(value) || !isFinite(value)) return '0,00 zł';
|
||||||
|
return value.toFixed(2).replace('.', ',') + ' zł';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the index of a table header by its text content
|
||||||
|
* @param {NodeList} headers - List of header elements
|
||||||
|
* @param {string} text - Text to search for
|
||||||
|
* @returns {number} - Index of the header or -1 if not found
|
||||||
|
*/
|
||||||
|
const findHeaderIndex = (headers, text) => {
|
||||||
|
for (let i = 0; i < headers.length; i++) {
|
||||||
|
if (headers[i].textContent.trim() === text) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates cost per item (Koszt / Sprzedane sztuki)
|
||||||
|
* @param {number} totalCost - Total cost value
|
||||||
|
* @param {number} soldItems - Number of sold items
|
||||||
|
* @returns {number} - Cost per item rounded to 2 decimal places
|
||||||
|
*/
|
||||||
|
const calculateCostPerItem = (totalCost, soldItems) => {
|
||||||
|
if (soldItems === 0) return 0;
|
||||||
|
return Math.round((totalCost / soldItems) * 100) / 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables other column sorting and hides their sort icons
|
||||||
|
*/
|
||||||
|
const disableOtherSorting = () => {
|
||||||
|
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
|
||||||
|
if (!scrollableTable) return;
|
||||||
|
|
||||||
|
const headers = scrollableTable.querySelectorAll('thead th:not([data-custom-column="koszt-1-szt"])');
|
||||||
|
headers.forEach(header => {
|
||||||
|
header.style.pointerEvents = 'none';
|
||||||
|
header.style.opacity = '0.5';
|
||||||
|
|
||||||
|
// Hide sort icons in other columns
|
||||||
|
const sortIcon = header.querySelector('.Table-module__table-header__sort-icon__Kx7KdyE');
|
||||||
|
if (sortIcon) {
|
||||||
|
sortIcon.style.display = 'none';
|
||||||
|
sortIcon.setAttribute('data-was-visible', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables other column sorting and restores their sort icons
|
||||||
|
*/
|
||||||
|
const enableOtherSorting = () => {
|
||||||
|
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
|
||||||
|
if (!scrollableTable) return;
|
||||||
|
|
||||||
|
const headers = scrollableTable.querySelectorAll('thead th:not([data-custom-column="koszt-1-szt"])');
|
||||||
|
headers.forEach(header => {
|
||||||
|
header.style.pointerEvents = '';
|
||||||
|
header.style.opacity = '';
|
||||||
|
|
||||||
|
// Restore sort icons in other columns
|
||||||
|
const sortIcon = header.querySelector('.Table-module__table-header__sort-icon__Kx7KdyE');
|
||||||
|
if (sortIcon && sortIcon.getAttribute('data-was-visible') === 'true') {
|
||||||
|
sortIcon.style.display = '';
|
||||||
|
sortIcon.removeAttribute('data-was-visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts table rows by cost per item
|
||||||
|
* @param {string} direction - 'asc' or 'desc'
|
||||||
|
*/
|
||||||
|
const sortTableByCosztPerItem = (direction) => {
|
||||||
|
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
|
||||||
|
if (!scrollableTable) return;
|
||||||
|
|
||||||
|
const tbody = scrollableTable.querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
|
||||||
|
// Separate summary row (first row) from data rows
|
||||||
|
const summaryRow = rows[0]?.classList.contains('Table-module__table-summary__ayBWA2V') ? rows[0] : null;
|
||||||
|
const dataRows = summaryRow ? rows.slice(1) : rows;
|
||||||
|
|
||||||
|
// Sort data rows
|
||||||
|
dataRows.sort((rowA, rowB) => {
|
||||||
|
const cellA = rowA.querySelector('td[data-custom-column="koszt-1-szt"]');
|
||||||
|
const cellB = rowB.querySelector('td[data-custom-column="koszt-1-szt"]');
|
||||||
|
|
||||||
|
if (!cellA || !cellB) return 0;
|
||||||
|
|
||||||
|
const valueA = parseCurrency(cellA.textContent);
|
||||||
|
const valueB = parseCurrency(cellB.textContent);
|
||||||
|
|
||||||
|
if (direction === 'asc') {
|
||||||
|
return valueA - valueB;
|
||||||
|
} else {
|
||||||
|
return valueB - valueA;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear tbody
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
// Re-add rows
|
||||||
|
if (summaryRow) {
|
||||||
|
tbody.appendChild(summaryRow);
|
||||||
|
}
|
||||||
|
dataRows.forEach(row => tbody.appendChild(row));
|
||||||
|
|
||||||
|
console.log(`Table sorted by Koszt 1 szt (${direction})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the "Koszt 1 szt" column to the table
|
||||||
|
*/
|
||||||
|
const addCostPerItemColumn = () => {
|
||||||
|
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
|
||||||
|
|
||||||
|
if (!scrollableTable) {
|
||||||
|
console.log('Table not found, retrying...');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thead = scrollableTable.querySelector('thead tr');
|
||||||
|
const tbody = scrollableTable.querySelector('tbody');
|
||||||
|
|
||||||
|
if (!thead || !tbody) {
|
||||||
|
console.log('Table structure not found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = thead.querySelectorAll('th');
|
||||||
|
const roasIndex = findHeaderIndex(headers, 'ROAS');
|
||||||
|
|
||||||
|
if (roasIndex === -1) {
|
||||||
|
console.log('ROAS column not found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if column already exists
|
||||||
|
let existingHeader = scrollableTable.querySelector('thead th[data-custom-column="koszt-1-szt"]');
|
||||||
|
|
||||||
|
if (!existingHeader) {
|
||||||
|
// Add new header after ROAS
|
||||||
|
const newHeader = document.createElement('th');
|
||||||
|
newHeader.className = 'Table-module__table-header__cpMic9I Utils-module__cursorPointer__ugRHHx8';
|
||||||
|
newHeader.setAttribute('data-custom-column', 'koszt-1-szt');
|
||||||
|
newHeader.setAttribute('data-sort-direction', 'none');
|
||||||
|
|
||||||
|
// Create header content with tooltip
|
||||||
|
const headerContent = document.createElement('div');
|
||||||
|
headerContent.style.display = 'flex';
|
||||||
|
headerContent.style.alignItems = 'center';
|
||||||
|
headerContent.style.justifyContent = 'space-between';
|
||||||
|
headerContent.style.width = '100%';
|
||||||
|
|
||||||
|
const headerText = document.createElement('span');
|
||||||
|
headerText.textContent = 'Koszt 1 szt';
|
||||||
|
headerText.style.flex = '1';
|
||||||
|
|
||||||
|
// Copy tooltip implementation from other columns by examining existing headers
|
||||||
|
const currentExistingHeaders = scrollableTable.querySelectorAll('thead th');
|
||||||
|
if (currentExistingHeaders.length > 0) {
|
||||||
|
// Copy classes from the first existing header
|
||||||
|
const firstHeader = currentExistingHeaders[0];
|
||||||
|
const classes = firstHeader.className;
|
||||||
|
newHeader.className = classes;
|
||||||
|
|
||||||
|
// Copy exact styles from the header itself, not its children
|
||||||
|
const computedStyle = window.getComputedStyle(firstHeader);
|
||||||
|
headerText.style.fontSize = computedStyle.fontSize;
|
||||||
|
headerText.style.fontFamily = computedStyle.fontFamily;
|
||||||
|
headerText.style.fontWeight = computedStyle.fontWeight;
|
||||||
|
headerText.style.lineHeight = computedStyle.lineHeight;
|
||||||
|
headerText.style.color = computedStyle.color;
|
||||||
|
headerText.style.fontStyle = computedStyle.fontStyle;
|
||||||
|
headerText.style.textTransform = computedStyle.textTransform;
|
||||||
|
headerText.style.letterSpacing = computedStyle.letterSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create custom tooltip that appears above the element
|
||||||
|
const tooltipText = 'Wyliczenie kosztów reklamy jednej sprzedanej sztuki. Podzielono Koszt przez sprzedane sztuki.\n\nAby przesortować kolumnę naciśnij raz aby sortować od największej do namniejszej wartości, naciśnij drugi raz aby sortować odwrotnie a trzeci raz aby inne kolumny miały ponownie możliwość sortowania.';
|
||||||
|
|
||||||
|
// Add mouse events for custom tooltip
|
||||||
|
let tooltipDiv = null;
|
||||||
|
|
||||||
|
newHeader.addEventListener('mouseenter', (e) => {
|
||||||
|
// Create tooltip div
|
||||||
|
tooltipDiv = document.createElement('div');
|
||||||
|
tooltipDiv.textContent = tooltipText;
|
||||||
|
tooltipDiv.style.position = 'fixed';
|
||||||
|
tooltipDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||||
|
tooltipDiv.style.color = 'white';
|
||||||
|
tooltipDiv.style.padding = '8px 12px';
|
||||||
|
tooltipDiv.style.borderRadius = '4px';
|
||||||
|
tooltipDiv.style.fontSize = '12px';
|
||||||
|
tooltipDiv.style.whiteSpace = 'pre-line';
|
||||||
|
tooltipDiv.style.zIndex = '9999';
|
||||||
|
tooltipDiv.style.maxWidth = '300px';
|
||||||
|
tooltipDiv.style.pointerEvents = 'none';
|
||||||
|
|
||||||
|
// Position above the element
|
||||||
|
const rect = newHeader.getBoundingClientRect();
|
||||||
|
tooltipDiv.style.left = (rect.left + rect.width / 2 - tooltipDiv.offsetWidth / 2) + 'px';
|
||||||
|
tooltipDiv.style.top = (rect.top - tooltipDiv.offsetHeight - 5) + 'px';
|
||||||
|
|
||||||
|
document.body.appendChild(tooltipDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
newHeader.addEventListener('mouseleave', () => {
|
||||||
|
if (tooltipDiv) {
|
||||||
|
tooltipDiv.remove();
|
||||||
|
tooltipDiv = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create sort icon container (hidden by default)
|
||||||
|
const sortIconContainer = document.createElement('div');
|
||||||
|
sortIconContainer.className = 'Table-module__table-header__sort-icon__Kx7KdyE';
|
||||||
|
sortIconContainer.style.display = 'none';
|
||||||
|
sortIconContainer.setAttribute('data-sort-icon', 'true');
|
||||||
|
|
||||||
|
// Create SVG arrow (like Allegro original)
|
||||||
|
const arrowDiv = document.createElement('div');
|
||||||
|
arrowDiv.className = 'mp7g_oh m0s5_e6 ArrowIcon-module__arrowDown__HSH8I9k';
|
||||||
|
arrowDiv.style.width = '22px';
|
||||||
|
arrowDiv.style.height = '22px';
|
||||||
|
arrowDiv.setAttribute('title', 'Sortuj');
|
||||||
|
|
||||||
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
svg.setAttribute('class', 'mp7g_f6 m7er_k4 mse2_k4 mg9e_0 mvrt_0 mj7a_0 mh36_0 mq1m_0 mj7u_0');
|
||||||
|
svg.style.width = '22px';
|
||||||
|
svg.style.height = '22px';
|
||||||
|
|
||||||
|
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
|
||||||
|
image.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', 'https://a.allegroimg.com/original/3424ea/a70a9cfd4ee59ddb6d4fc30364c7/action-common-arrowhead-c3c511fba9');
|
||||||
|
image.setAttribute('href', 'https://a.allegroimg.com/original/3424ea/a70a9cfd4ee59ddb6d4fc30364c7/action-common-arrowhead-c3c511fba9');
|
||||||
|
image.setAttribute('width', '22px');
|
||||||
|
image.setAttribute('height', '22px');
|
||||||
|
image.setAttribute('data-testid', 'svg-image-element');
|
||||||
|
|
||||||
|
svg.appendChild(image);
|
||||||
|
arrowDiv.appendChild(svg);
|
||||||
|
sortIconContainer.appendChild(arrowDiv);
|
||||||
|
|
||||||
|
headerContent.appendChild(headerText);
|
||||||
|
headerContent.appendChild(sortIconContainer);
|
||||||
|
newHeader.appendChild(headerContent);
|
||||||
|
|
||||||
|
// Add click handler for sorting
|
||||||
|
newHeader.addEventListener('click', () => {
|
||||||
|
const currentDirection = newHeader.getAttribute('data-sort-direction');
|
||||||
|
let newDirection = 'desc';
|
||||||
|
|
||||||
|
if (currentDirection === 'none') {
|
||||||
|
// First click - sort descending (arrow pointing down)
|
||||||
|
newDirection = 'desc';
|
||||||
|
sortIconContainer.style.display = 'block';
|
||||||
|
arrowDiv.style.transform = 'rotate(90deg)';
|
||||||
|
disableOtherSorting();
|
||||||
|
} else if (currentDirection === 'desc') {
|
||||||
|
// Second click - sort ascending (arrow pointing up)
|
||||||
|
newDirection = 'asc';
|
||||||
|
arrowDiv.style.transform = 'rotate(-90deg)';
|
||||||
|
} else {
|
||||||
|
// Third click - reset sorting and restore original order
|
||||||
|
newDirection = 'none';
|
||||||
|
sortIconContainer.style.display = 'none';
|
||||||
|
enableOtherSorting();
|
||||||
|
newHeader.setAttribute('data-sort-direction', 'none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newHeader.setAttribute('data-sort-direction', newDirection);
|
||||||
|
sortTableByCosztPerItem(newDirection);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (roasIndex + 1 < headers.length) {
|
||||||
|
headers[roasIndex + 1].before(newHeader);
|
||||||
|
} else {
|
||||||
|
thead.appendChild(newHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
existingHeader = newHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process data cells for each row
|
||||||
|
const rows = tbody.querySelectorAll('tr');
|
||||||
|
rows.forEach((row) => {
|
||||||
|
// Check if this row already has our custom column
|
||||||
|
const existingCell = row.querySelector('td[data-custom-column="koszt-1-szt"]');
|
||||||
|
|
||||||
|
if (existingCell) {
|
||||||
|
// Remove it to recalculate with correct indices
|
||||||
|
existingCell.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cells = row.querySelectorAll('td');
|
||||||
|
|
||||||
|
// Get Koszt value (index 4 in scrollable table: 0-Kliknięcia, 1-Odsłony, 2-CTR, 3-Średnie CPC, 4-Koszt)
|
||||||
|
const kosztCell = cells[4];
|
||||||
|
// Get Sprzedane sztuki value (index 7 in scrollable table: 5-ROAS, 6-Zainteresowanie, 7-Sprzedane sztuki)
|
||||||
|
const sprzedaneCell = cells[7];
|
||||||
|
|
||||||
|
let costPerItemValue = '0,00 zł';
|
||||||
|
|
||||||
|
if (kosztCell && sprzedaneCell) {
|
||||||
|
const kosztText = kosztCell.textContent.trim();
|
||||||
|
// Handle button elements in sprzedane sztuki column
|
||||||
|
const sprzedaneButton = sprzedaneCell.querySelector('button');
|
||||||
|
const sprzedaneText = sprzedaneButton ? sprzedaneButton.textContent.trim() : sprzedaneCell.textContent.trim();
|
||||||
|
|
||||||
|
const totalCost = parseCurrency(kosztText);
|
||||||
|
const soldItems = parseInteger(sprzedaneText);
|
||||||
|
|
||||||
|
const costPerItem = calculateCostPerItem(totalCost, soldItems);
|
||||||
|
costPerItemValue = formatCurrency(costPerItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCell = document.createElement('td');
|
||||||
|
newCell.className = 'StatisticsTable-module__statistics-table__cell__x1ILnxF Table-module__table-cell__LI2CMBo Table-module__table-cell--align-right__kyh17WP';
|
||||||
|
newCell.textContent = costPerItemValue;
|
||||||
|
newCell.setAttribute('data-custom-column', 'koszt-1-szt');
|
||||||
|
|
||||||
|
// Find ROAS cell again after removal
|
||||||
|
const currentCells = row.querySelectorAll('td');
|
||||||
|
const currentRoasIndex = 5; // ROAS is at index 5 in scrollable table
|
||||||
|
|
||||||
|
if (currentRoasIndex + 1 < currentCells.length) {
|
||||||
|
currentCells[currentRoasIndex + 1].before(newCell);
|
||||||
|
} else {
|
||||||
|
row.appendChild(newCell);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Koszt 1 szt column added successfully');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observes DOM changes and adds the column when table is loaded
|
||||||
|
*/
|
||||||
|
const observeTableChanges = () => {
|
||||||
|
const targetNode = document.body;
|
||||||
|
const config = { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-selected'] };
|
||||||
|
|
||||||
|
let isProcessing = false;
|
||||||
|
let processingTimeout = null;
|
||||||
|
|
||||||
|
const callback = (mutationsList, observer) => {
|
||||||
|
// Avoid infinite loop - skip if already processing
|
||||||
|
if (isProcessing) return;
|
||||||
|
|
||||||
|
// Check for tab changes or table additions
|
||||||
|
for (const mutation of mutationsList) {
|
||||||
|
// Check for tab panel attribute changes (tab switching)
|
||||||
|
if (mutation.type === 'attributes' && mutation.attributeName === 'aria-selected') {
|
||||||
|
const target = mutation.target;
|
||||||
|
if (target.getAttribute('aria-selected') === 'true' && target.getAttribute('role') === 'tabpanel') {
|
||||||
|
console.log('Tab switched, waiting for table to load...');
|
||||||
|
isProcessing = true;
|
||||||
|
|
||||||
|
// Clear previous timeout
|
||||||
|
if (processingTimeout) {
|
||||||
|
clearTimeout(processingTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for table to load after tab switch
|
||||||
|
processingTimeout = setTimeout(() => {
|
||||||
|
addCostPerItemColumn();
|
||||||
|
isProcessing = false;
|
||||||
|
}, 800);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for table additions
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
const tableExists = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
|
||||||
|
if (tableExists) {
|
||||||
|
isProcessing = true;
|
||||||
|
|
||||||
|
// Clear previous timeout
|
||||||
|
if (processingTimeout) {
|
||||||
|
clearTimeout(processingTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
processingTimeout = setTimeout(() => {
|
||||||
|
addCostPerItemColumn();
|
||||||
|
isProcessing = false;
|
||||||
|
}, 500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new MutationObserver(callback);
|
||||||
|
observer.observe(targetNode, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the script
|
||||||
|
*/
|
||||||
|
const init = () => {
|
||||||
|
console.log('Allegro Sales Center - Koszt 1 szt Calculator initialized');
|
||||||
|
|
||||||
|
// Try to add column immediately
|
||||||
|
setTimeout(() => {
|
||||||
|
addCostPerItemColumn();
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Observe for future changes (pagination, filters, etc.)
|
||||||
|
observeTableChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the script
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
318
idea/idea-edi-tim.user.js
Normal file
318
idea/idea-edi-tim.user.js
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name IDEAERP - Dodaj sekcję EMMA z TIM EDI
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.0
|
||||||
|
// @description Dodaje sekcję "EMMA" do menu głównego z funkcjonalnością generowania plików EDI dla TIM. Po kliknięciu otwiera się modal z polem do wprowadzenia numeru zamówienia i wysyłania danych do serwera TIM EDI. Automatyczna konwersja liter na wielkie.
|
||||||
|
// @match https://emma.ideaerp.pl/web*
|
||||||
|
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/edi-tim
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/edi-tim
|
||||||
|
// @grant none
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ========================OPIS==============================
|
||||||
|
WERSJA 1.0 - Integracja z TIM EDI + Automatyczne aktualizacje
|
||||||
|
|
||||||
|
CEL SKRYPTU:
|
||||||
|
- Dodanie sekcji "EMMA" do menu głównego IDEAERP
|
||||||
|
- Integracja z systemem TIM EDI do generowania plików
|
||||||
|
- Umożliwienie wysyłania numerów zamówień do serwera TIM
|
||||||
|
|
||||||
|
DZIAŁANIE SKRYPTU:
|
||||||
|
1. Lokalizuje sekcję menu o ID "209" w systemie IDEAERP
|
||||||
|
2. Tworzy nową sekcję "EMMA" z elementem "Tim Edi"
|
||||||
|
3. Po kliknięciu otwiera modal z formularzem
|
||||||
|
4. Umożliwia wprowadzenie numeru zamówienia
|
||||||
|
5. Wysyła dane do serwera TIM EDI (https://192.168.0.97:5011/tim/edi)
|
||||||
|
6. Czyści formularz po udanym wysłaniu
|
||||||
|
|
||||||
|
WYMAGANIA TECHNICZNE:
|
||||||
|
- Dostęp do serwera TIM EDI (192.168.0.97:5011)
|
||||||
|
- Sekcja menu o data-menu-section="209" w IDEAERP
|
||||||
|
- Obsługa fetch API w przeglądarce
|
||||||
|
- Dostęp do serwera aktualizacji (n8n.emma.net.pl/webhook/edi-tim)
|
||||||
|
- Tampermonkey z obsługą automatycznych aktualizacji
|
||||||
|
|
||||||
|
FUNKCJONALNOŚCI:
|
||||||
|
+ Dodawanie sekcji EMMA do menu głównego
|
||||||
|
+ Modal z formularzem do wprowadzania numeru zamówienia
|
||||||
|
+ Automatyczna konwersja liter na wielkie (w czasie rzeczywistym)
|
||||||
|
+ Zabezpieczenie przed wklejaniem małych liter
|
||||||
|
+ Walidacja wprowadzonych danych
|
||||||
|
+ Wysyłanie POST request do serwera TIM EDI
|
||||||
|
+ Obsługa błędów sieciowych
|
||||||
|
+ Czyszczenie formularza po wysłaniu
|
||||||
|
+ Przycisk zamknięcia modala (X)
|
||||||
|
+ Overlay z efektem przyciemnienia tła
|
||||||
|
|
||||||
|
STRUKTURA MODALA:
|
||||||
|
- Tytuł: "Generuj plik EDI"
|
||||||
|
- Pole tekstowe z placeholder: "Podaj numer zamówienia"
|
||||||
|
- Przycisk "Wyślij" (niebieski)
|
||||||
|
- Przycisk zamknięcia (X w prawym górnym rogu)
|
||||||
|
- Overlay z półprzezroczystym tłem
|
||||||
|
|
||||||
|
ENDPOINT API:
|
||||||
|
- URL: https://192.168.0.97:5011/tim/edi
|
||||||
|
- Method: POST
|
||||||
|
- Content-Type: application/json
|
||||||
|
- Body: { "Zamówienie": "numer_zamówienia" }
|
||||||
|
|
||||||
|
OBSŁUGA BŁĘDÓW:
|
||||||
|
- Walidacja pustego pola (alert: "Dodaj zamówienia")
|
||||||
|
- Obsługa błędów sieciowych (alert: "Wystąpił błąd podczas wysyłania danych")
|
||||||
|
- Logowanie błędów do konsoli
|
||||||
|
|
||||||
|
INTEGRACJA Z IDEAERP:
|
||||||
|
- Wstrzykiwanie sekcji do istniejącego menu
|
||||||
|
- Umieszczenie sekcji EMMA jako pierwszy element w kontenerze
|
||||||
|
- Zachowanie stylów i struktury menu IDEAERP
|
||||||
|
- Obsługa dynamicznego ładowania strony
|
||||||
|
|
||||||
|
AUTOMATYCZNE AKTUALIZACJE:
|
||||||
|
- Serwer aktualizacji: https://n8n.emma.net.pl/webhook/edi-tim
|
||||||
|
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
|
||||||
|
- Jednoklikowa aktualizacja bez konieczności ręcznego pobierania
|
||||||
|
- Zachowanie ustawień użytkownika podczas aktualizacji
|
||||||
|
- Kompatybilność z systemem n8n do zarządzania wersjami
|
||||||
|
|
||||||
|
CHANGELOG:
|
||||||
|
v1.0 (2024) - AUTOMATYCZNE AKTUALIZACJE
|
||||||
|
- Dodano @downloadURL i @updateURL wskazujące na n8n.emma.net.pl/webhook/edi-tim
|
||||||
|
- Integracja z systemem n8n do zarządzania wersjami skryptu
|
||||||
|
- Automatyczne sprawdzanie i pobieranie aktualizacji przez Tampermonkey
|
||||||
|
- Dokumentacja procesu aktualizacji w sekcji OPIS
|
||||||
|
- Podwyższenie numeru wersji do 1.0 (stabilna wersja)
|
||||||
|
|
||||||
|
v0.9 (2024) - AUTOMATYCZNA KONWERSJA LITER
|
||||||
|
- Dodano automatyczną konwersję liter na wielkie w czasie rzeczywistym
|
||||||
|
- Zabezpieczenie przed wklejaniem małych liter
|
||||||
|
- Zachowanie pozycji kursora podczas konwersji
|
||||||
|
- Dodano CSS text-transform: uppercase dla wizualnej spójności
|
||||||
|
|
||||||
|
v0.8 (2024) - PIERWSZA WERSJA
|
||||||
|
- Dodanie sekcji EMMA do menu głównego
|
||||||
|
- Implementacja modala z formularzem
|
||||||
|
- Integracja z serwerem TIM EDI
|
||||||
|
- Obsługa wysyłania numerów zamówień
|
||||||
|
- Walidacja danych wejściowych
|
||||||
|
- Obsługa błędów sieciowych
|
||||||
|
- Czyszczenie formularza po wysłaniu
|
||||||
|
- Responsywny design modala
|
||||||
|
- Przycisk zamknięcia modala
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ——— Funkcja główna: dodawanie sekcji EMMA do menu ———
|
||||||
|
function addEmmaSection() {
|
||||||
|
var targetSection = document.querySelector('div.i_secondary_menu__section[data-menu-section="209"]');
|
||||||
|
if (!targetSection) {
|
||||||
|
setTimeout(addEmmaSection, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (document.querySelector('div.i_secondary_menu__section[data-menu-section="1248"]')) {
|
||||||
|
attachPopupListener();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSectionHtml = `
|
||||||
|
<div class="i_secondary_menu__section folded" data-menu-section="1248">
|
||||||
|
<a href="#" class="i_secondary_menu__toggle o_menu_header_lvl_1" data-menu-xmlid="ideaerp_base.i7_base_sale_order_folders_main_menu" data-display="static" role="button" aria-expanded="false">
|
||||||
|
EMMA
|
||||||
|
</a>
|
||||||
|
<ul class="i_secondary_menu__content" role="menu">
|
||||||
|
<li>
|
||||||
|
<a role="menuitem" href="#" class="dropdown-item o_menu_entry_lvl_2" id="timEdiLink">
|
||||||
|
<span>Tim Edi</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
var tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = newSectionHtml.trim();
|
||||||
|
var newSection = tempDiv.firstElementChild;
|
||||||
|
// Wstawiamy sekcję jako pierwszy element w kontenerze
|
||||||
|
var parent = targetSection.parentNode;
|
||||||
|
parent.insertBefore(newSection, parent.firstChild);
|
||||||
|
console.log("Dodano sekcję EMMA z TIM EDI do menu jako pierwszy element.");
|
||||||
|
attachPopupListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Obsługa kliknięć: podpięcie event listenera do linku TIM EDI ———
|
||||||
|
function attachPopupListener() {
|
||||||
|
var timEdiLink = document.getElementById('timEdiLink');
|
||||||
|
if (timEdiLink) {
|
||||||
|
timEdiLink.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
showPopup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Tworzenie i wyświetlanie modala z formularzem TIM EDI ———
|
||||||
|
function showPopup() {
|
||||||
|
var overlay = document.getElementById('edi-popup-overlay');
|
||||||
|
if (!overlay) {
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.id = 'edi-popup-overlay';
|
||||||
|
Object.assign(overlay.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
right: '0',
|
||||||
|
bottom: '0',
|
||||||
|
background: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
zIndex: '10000',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
});
|
||||||
|
|
||||||
|
var modal = document.createElement('div');
|
||||||
|
modal.id = 'edi-popup';
|
||||||
|
Object.assign(modal.style, {
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '20px',
|
||||||
|
width: '320px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
|
||||||
|
position: 'relative',
|
||||||
|
fontFamily: 'Arial, sans-serif'
|
||||||
|
});
|
||||||
|
|
||||||
|
// ——— Przycisk zamknięcia modala (X w prawym górnym rogu) ———
|
||||||
|
var closeButton = document.createElement('button');
|
||||||
|
closeButton.id = 'edi-close';
|
||||||
|
closeButton.textContent = '×';
|
||||||
|
Object.assign(closeButton.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '1px',
|
||||||
|
right: '5px',
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '22px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#888'
|
||||||
|
});
|
||||||
|
closeButton.addEventListener('click', function() {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
});
|
||||||
|
modal.appendChild(closeButton);
|
||||||
|
|
||||||
|
// ——— Tytuł modala ———
|
||||||
|
var title = document.createElement('h4');
|
||||||
|
title.textContent = 'Generuj plik EDI';
|
||||||
|
Object.assign(title.style, {
|
||||||
|
margin: '20px 0 15px 0',
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#888'
|
||||||
|
});
|
||||||
|
modal.appendChild(title);
|
||||||
|
|
||||||
|
// ——— Pole tekstowe do wprowadzania numeru zamówienia ———
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = 'order-number';
|
||||||
|
input.placeholder = 'Podaj numer zamówienia';
|
||||||
|
Object.assign(input.style, {
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px',
|
||||||
|
marginBottom: '15px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px',
|
||||||
|
textTransform: 'uppercase' // Automatyczna konwersja na wielkie litery
|
||||||
|
});
|
||||||
|
|
||||||
|
// ——— Zabezpieczenie: automatyczna konwersja na wielkie litery ———
|
||||||
|
input.addEventListener('input', function(e) {
|
||||||
|
var cursorPosition = e.target.selectionStart;
|
||||||
|
var originalValue = e.target.value;
|
||||||
|
var upperValue = originalValue.toUpperCase();
|
||||||
|
|
||||||
|
// Aktualizujemy wartość tylko jeśli nastąpiła zmiana
|
||||||
|
if (originalValue !== upperValue) {
|
||||||
|
e.target.value = upperValue;
|
||||||
|
// Przywracamy pozycję kursora
|
||||||
|
e.target.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ——— Dodatkowe zabezpieczenie dla wklejania tekstu ———
|
||||||
|
input.addEventListener('paste', function(e) {
|
||||||
|
setTimeout(function() {
|
||||||
|
var cursorPosition = input.selectionStart;
|
||||||
|
var originalValue = input.value;
|
||||||
|
var upperValue = originalValue.toUpperCase();
|
||||||
|
|
||||||
|
if (originalValue !== upperValue) {
|
||||||
|
input.value = upperValue;
|
||||||
|
input.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.appendChild(input);
|
||||||
|
|
||||||
|
// ——— Przycisk wysyłania danych do serwera TIM EDI ———
|
||||||
|
var button = document.createElement('button');
|
||||||
|
button.id = 'edi-submit';
|
||||||
|
button.textContent = 'Wyślij';
|
||||||
|
Object.assign(button.style, {
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: '14px',
|
||||||
|
background: '#007bff',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
});
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
// ——— Walidacja danych wejściowych ———
|
||||||
|
var orderNumber = input.value.trim();
|
||||||
|
if (!orderNumber) {
|
||||||
|
alert("Dodaj zamówienia");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ——— Wysyłanie danych do serwera TIM EDI ———
|
||||||
|
fetch('https://192.168.0.97:5011/tim/edi', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ "Zamówienie": orderNumber })
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// ——— Obsługa sukcesu: czyszczenie formularza i zamknięcie modala ———
|
||||||
|
console.log('Sukces:', data);
|
||||||
|
input.value = "";
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// ——— Obsługa błędów sieciowych ———
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert("Wystąpił błąd podczas wysyłania danych.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
modal.appendChild(button);
|
||||||
|
|
||||||
|
overlay.appendChild(modal);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
} else {
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Inicjalizacja: uruchomienie po załadowaniu strony ———
|
||||||
|
window.addEventListener('load', addEmmaSection);
|
||||||
|
})();
|
||||||
276
idea/idea-invoice-famica.user.js
Normal file
276
idea/idea-invoice-famica.user.js
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Idea Invoice Famica
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 0.2
|
||||||
|
// @description Rozszerza funkcjonalność faktur IdeaERP o dodawanie numeru EORI i ulepszone drukowanie/generowanie PDF dla faktur Famica Ltd.
|
||||||
|
// @author You
|
||||||
|
// @match https://emma.ideaerp.pl/my/invoices*
|
||||||
|
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/invoice-famica
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/invoice-famica
|
||||||
|
// @grant none
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IDEA INVOICE FAMICA - Skrypt Rozszerzający Przetwarzanie Faktur
|
||||||
|
*
|
||||||
|
* Ten skrypt użytkownika Tampermonkey rozszerza interfejs faktur IdeaERP poprzez
|
||||||
|
* dodanie trzech niestandardowych przycisków usprawniających workflow przetwarzania faktur.
|
||||||
|
*
|
||||||
|
* FUNKCJE:
|
||||||
|
* - Dodaje numer EORI (GB174521806000) do informacji o nabywcy na fakturach
|
||||||
|
* - Ulepszone drukowanie z prawidłową widocznością nagłówków tabel i cieniowania
|
||||||
|
* - Funkcjonalność generowania PDF z zachowaniem formatowania
|
||||||
|
*
|
||||||
|
* SZCZEGÓŁY TECHNICZNE:
|
||||||
|
* - Działa w systemie wyświetlania faktur opartym na iframe
|
||||||
|
* - Używa wstrzykiwania CSS do wymuszenia dostosowania kolorów przy drukowaniu
|
||||||
|
* - Implementuje obsługę MediaQueryList kompatybilną z różnymi przeglądarkami
|
||||||
|
* - Zapobiega duplikowaniu przycisków poprzez znaczniki dataset
|
||||||
|
*
|
||||||
|
* SPOSÓB UŻYCIA:
|
||||||
|
* 1. "Dodaj EORI" - Dodaje numer EORI do sekcji nabywcy na fakturze
|
||||||
|
* 2. "Wydrukuj z EORI" - Otwiera okno drukowania z ulepszonym formatowaniem
|
||||||
|
* 3. "Pobierz z EORI" - Prowadzi użytkownika przez zapisanie faktury jako PDF z formatowaniem
|
||||||
|
*
|
||||||
|
* HISTORIA ZMIAN:
|
||||||
|
* v0.2 (2025-09-24)
|
||||||
|
* - Dodano ulepszoną funkcjonalność drukowania z poprawkami widoczności nagłówków tabel
|
||||||
|
* - Zaimplementowano zachowanie cieniowania wierszy w wydruku
|
||||||
|
* - Dodano funkcjonalność pobierania PDF z instrukcją dla użytkownika
|
||||||
|
* - Naprawiono problemy kompatybilności MediaQueryList między przeglądarkami
|
||||||
|
* - Ulepszona obsługa błędów i mechanizmy czyszczenia
|
||||||
|
*
|
||||||
|
* v0.1 (2025-09-24)
|
||||||
|
* - Pierwsze wydanie z podstawową funkcjonalnością dodawania EORI
|
||||||
|
* - Podstawowe tworzenie przycisków i manipulacja zawartością iframe
|
||||||
|
* - Implementacja MutationObserver do wykrywania dynamicznej zawartości
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function addButtons() {
|
||||||
|
const targetDivs = document.querySelectorAll('.o_download_pdf.btn-toolbar');
|
||||||
|
|
||||||
|
targetDivs.forEach(targetDiv => {
|
||||||
|
// Check if buttons are already added
|
||||||
|
if (targetDiv.dataset.customButtonsAdded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create "Dodaj EORI" button
|
||||||
|
const eoriButtonGroup = document.createElement('div');
|
||||||
|
eoriButtonGroup.className = 'btn-group flex-grow-1 mr-1 mb-1';
|
||||||
|
const eoriButton = document.createElement('a');
|
||||||
|
eoriButton.className = 'btn btn-secondary btn-block';
|
||||||
|
eoriButton.href = '#';
|
||||||
|
eoriButton.innerHTML = '<i class="fa fa-plus"></i> Dodaj EORI';
|
||||||
|
eoriButton.onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const iframe = document.getElementById('invoice_html');
|
||||||
|
if (!iframe || !iframe.contentWindow) {
|
||||||
|
alert('Ramka z fakturą nie została znaleziona.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
const nabywcaDiv = iframeDoc.querySelector('.i_nabywca.col-4');
|
||||||
|
|
||||||
|
if (!nabywcaDiv) {
|
||||||
|
alert('Sekcja nabywcy nie została znaleziona w fakturze.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if EORI is already added
|
||||||
|
if (iframeDoc.querySelector('.eori-number')) {
|
||||||
|
alert('Numer EORI został już dodany.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const br = iframeDoc.createElement('br');
|
||||||
|
const eoriTextNode = iframeDoc.createTextNode('EORI Number: ');
|
||||||
|
const eoriSpan = iframeDoc.createElement('span');
|
||||||
|
eoriSpan.className = 'eori-number';
|
||||||
|
eoriSpan.innerText = 'GB174521806000';
|
||||||
|
|
||||||
|
nabywcaDiv.appendChild(br);
|
||||||
|
nabywcaDiv.appendChild(eoriTextNode);
|
||||||
|
nabywcaDiv.appendChild(eoriSpan);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
eoriButtonGroup.appendChild(eoriButton);
|
||||||
|
|
||||||
|
// Create "Wydrukuj z EORI" button
|
||||||
|
const printEoriButtonGroup = document.createElement('div');
|
||||||
|
printEoriButtonGroup.className = 'btn-group flex-grow-1 mb-1';
|
||||||
|
const printEoriButton = document.createElement('a');
|
||||||
|
printEoriButton.className = 'btn btn-secondary btn-block';
|
||||||
|
printEoriButton.href = '#';
|
||||||
|
printEoriButton.innerHTML = '<i class="fa fa-print"></i> Wydrukuj z EORI';
|
||||||
|
printEoriButton.onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const iframe = document.getElementById('invoice_html');
|
||||||
|
if (iframe && iframe.contentWindow) {
|
||||||
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
const iframeWin = iframe.contentWindow;
|
||||||
|
|
||||||
|
// Inject style to force printing background colors and shadows
|
||||||
|
const style = iframeDoc.createElement('style');
|
||||||
|
style.id = 'print-fix-styles';
|
||||||
|
style.innerHTML = `
|
||||||
|
@media print {
|
||||||
|
body, body * {
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
}
|
||||||
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: #f9f9f9 !important;
|
||||||
|
}
|
||||||
|
thead, thead *, thead th, thead th * {
|
||||||
|
color: black !important;
|
||||||
|
-webkit-text-fill-color: black !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
iframeDoc.head.appendChild(style);
|
||||||
|
|
||||||
|
const removePrintStyles = () => {
|
||||||
|
const styleToRemove = iframeDoc.getElementById('print-fix-styles');
|
||||||
|
if (styleToRemove) {
|
||||||
|
iframeDoc.head.removeChild(styleToRemove);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mediaQueryList = iframeWin.matchMedia('print');
|
||||||
|
const handler = (mql) => {
|
||||||
|
if (!mql.matches) {
|
||||||
|
removePrintStyles();
|
||||||
|
// Use the correct method based on browser support
|
||||||
|
try {
|
||||||
|
if (mql.removeEventListener) {
|
||||||
|
mql.removeEventListener('change', handler);
|
||||||
|
} else if (mql.removeListener) {
|
||||||
|
mql.removeListener(handler);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors when removing listeners
|
||||||
|
console.log('Error removing print listener:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mediaQueryList.addEventListener) {
|
||||||
|
mediaQueryList.addEventListener('change', handler);
|
||||||
|
} else if (mediaQueryList.addListener) {
|
||||||
|
mediaQueryList.addListener(handler);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error adding print listener:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
iframeWin.focus();
|
||||||
|
iframeWin.print();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
printEoriButtonGroup.appendChild(printEoriButton);
|
||||||
|
|
||||||
|
// Create "Pobierz z EORI" button
|
||||||
|
const downloadEoriButtonGroup = document.createElement('div');
|
||||||
|
downloadEoriButtonGroup.className = 'btn-group flex-grow-1 mr-1 mb-1';
|
||||||
|
const downloadEoriButton = document.createElement('a');
|
||||||
|
downloadEoriButton.className = 'btn btn-secondary btn-block';
|
||||||
|
downloadEoriButton.href = '#';
|
||||||
|
downloadEoriButton.innerHTML = '<i class="fa fa-download"></i> Pobierz z EORI';
|
||||||
|
downloadEoriButton.onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const iframe = document.getElementById('invoice_html');
|
||||||
|
if (iframe && iframe.contentWindow) {
|
||||||
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
const iframeWin = iframe.contentWindow;
|
||||||
|
|
||||||
|
// Inject style to force printing background colors and shadows
|
||||||
|
const style = iframeDoc.createElement('style');
|
||||||
|
style.id = 'download-fix-styles';
|
||||||
|
style.innerHTML = `
|
||||||
|
@media print {
|
||||||
|
body, body * {
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
}
|
||||||
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: #f9f9f9 !important;
|
||||||
|
}
|
||||||
|
thead, thead *, thead th, thead th * {
|
||||||
|
color: black !important;
|
||||||
|
-webkit-text-fill-color: black !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
iframeDoc.head.appendChild(style);
|
||||||
|
|
||||||
|
// Show instruction to user
|
||||||
|
alert('W oknie drukowania wybierz "Zapisz jako PDF" jako drukarkę docelową.');
|
||||||
|
|
||||||
|
// Create a function to handle PDF saving
|
||||||
|
const saveAsPdf = () => {
|
||||||
|
iframeWin.print();
|
||||||
|
|
||||||
|
// Clean up styles after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
const styleToRemove = iframeDoc.getElementById('download-fix-styles');
|
||||||
|
if (styleToRemove) {
|
||||||
|
iframeDoc.head.removeChild(styleToRemove);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
iframeWin.focus();
|
||||||
|
setTimeout(saveAsPdf, 500);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
downloadEoriButtonGroup.appendChild(downloadEoriButton);
|
||||||
|
|
||||||
|
// Append new buttons to the target div
|
||||||
|
targetDiv.appendChild(eoriButtonGroup);
|
||||||
|
targetDiv.appendChild(printEoriButtonGroup);
|
||||||
|
targetDiv.appendChild(downloadEoriButtonGroup);
|
||||||
|
|
||||||
|
// Mark as buttons added
|
||||||
|
targetDiv.dataset.customButtonsAdded = 'true';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use MutationObserver to detect when new content is added to the page
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
mutations.forEach(function(mutation) {
|
||||||
|
if (mutation.addedNodes.length) {
|
||||||
|
addButtons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
})();
|
||||||
981
idea/idea-kfz-listing.user.js
Normal file
981
idea/idea-kfz-listing.user.js
Normal file
|
|
@ -0,0 +1,981 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name IDEA KFZ Listing
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 3.1
|
||||||
|
// @description Ulepszenia dla listy faktur KFZ w systemie IDEA ERP
|
||||||
|
// @author Adam
|
||||||
|
// @match https://*.ideaerp.pl/*
|
||||||
|
// @icon https://www.ideaerp.pl/favicon.ico
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/kfz-listing
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/kfz-listing
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @run-at document-end
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ========================OPIS==============================
|
||||||
|
WERSJA 3.1 - Uproszczona logika wykrywania strony
|
||||||
|
|
||||||
|
DZIAŁANIE SKRYPTU:
|
||||||
|
- Wykrywa strony z listą faktur KFZ w systemie IDEA ERP
|
||||||
|
- Dodaje ulepszenia wizualne tabeli (sticky header, podświetlanie)
|
||||||
|
- Implementuje szybkie filtry według dostawców
|
||||||
|
- Umożliwia kopiowanie zawartości komórek w kolumnach:
|
||||||
|
* Numer faktury (data-name="name")
|
||||||
|
* Data faktury (data-name="invoice_date")
|
||||||
|
* Odnośnik (data-name="invoice_origin")
|
||||||
|
* Należność (data-name="amount_residual_signed" - NIE NETTO!)
|
||||||
|
* Na podstawie (data-name="reversed_entry_id")
|
||||||
|
- Pokazuje powiadomienia o skopiowaniu
|
||||||
|
|
||||||
|
ZMIANY W WERSJI 3.1:
|
||||||
|
- 🔍 UPROSZCZONA logika wykrywania strony - bardziej liberalne warunki
|
||||||
|
- ✅ Działaj jeśli: action=278+menu_id=141 LUB breadcrumb="Korekty" LUB numery KFZ
|
||||||
|
- 📊 Dodano szczegółowe logi w initEnhancements()
|
||||||
|
- 🚀 Usunięto zbędne skomplikowane warunki
|
||||||
|
- 🎯 Focus na działanie, nie na perfekcyjne wykrywanie
|
||||||
|
|
||||||
|
ZMIANY W WERSJI 3.0:
|
||||||
|
- 🔄 POWRÓT do stabilnej wersji bez waitForPageLoad()
|
||||||
|
- ❌ USUNIĘTO skomplikowaną funkcję waitForPageLoad() która powodowała błędy
|
||||||
|
- ✅ PRZYWRÓCONO prostą logikę z setTimeout(initEnhancements)
|
||||||
|
- 🎯 Wymagane action=278 + menu_id=141 dla aktywacji
|
||||||
|
- 📊 Zachowano podstawowe logi debugowania
|
||||||
|
- 🚀 Punkt wyjścia do dalszych ulepszeń
|
||||||
|
|
||||||
|
ZMIANY W WERSJI 2.9.1:
|
||||||
|
- 🐛 NAPRAWIONO błąd "hasAction278 is not defined"
|
||||||
|
- 🔧 Poprawiono wszystkie odwołania do zmiennych w waitForPageLoad()
|
||||||
|
- ✅ Usunięto błędy JavaScript które blokowały działanie skryptu
|
||||||
|
|
||||||
|
ZMIANY W WERSJI 2.9:
|
||||||
|
- 🔍 DODANO szczegółowe debugowanie w waitForPageLoad() i isKfzListingPage()
|
||||||
|
- 📊 Emoji w logach dla lepszej czytelności
|
||||||
|
- 🎯 Wymagane action=278 + menu_id=141 dla aktywacji
|
||||||
|
- 📍 Logi URL i elementów na starcie skryptu
|
||||||
|
- 🚀 Test czy skrypt w ogóle się ładuje
|
||||||
|
- 🔧 Diagnostyka dla problemu "nie działa w ogóle"
|
||||||
|
|
||||||
|
ZMIANY W WERSJI 2.8:
|
||||||
|
- 🔄 DODANO resetowanie stanu przy zmianie URL (hashchange i polling)
|
||||||
|
- 🔍 Sprawdzanie czy elementy rzeczywiście istnieją mimo markera
|
||||||
|
- 🧹 Automatyczne usuwanie markerów przy zmianie strony
|
||||||
|
- 🎯 Dodano hasKfzNumbers jako dodatkowy warunek aktywacji
|
||||||
|
- 📊 Szczegółowe logi resetowania stanu
|
||||||
|
- ✅ Rozwiązano problem niespójnego działania przy nawigacji
|
||||||
|
|
||||||
|
ZMIANY W WERSJI 2.7:
|
||||||
|
- 🕐 DODANO funkcję waitForPageLoad() - inteligentne oczekiwanie na elementy
|
||||||
|
- 🔄 Maksymalnie 10 prób co 1 sekundę na załadowanie tabeli/breadcrumb
|
||||||
|
- 🚫 Wykrywanie elementów ładowania (.o_loading, .fa-spin) - czeka 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 = '<i class="fa fa-filter"></i> Dostawca:';
|
||||||
|
label.style.cssText = `
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: normal;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Select z dostawcami - styl podobny do innych kontrolek Odoo
|
||||||
|
const supplierFilter = document.createElement('select');
|
||||||
|
supplierFilter.className = 'form-control';
|
||||||
|
supplierFilter.style.cssText = `
|
||||||
|
height: 32px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
background-color: white;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 250px;
|
||||||
|
line-height: 1.5;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Opcja "Wszyscy"
|
||||||
|
const allOption = document.createElement('option');
|
||||||
|
allOption.value = '';
|
||||||
|
allOption.textContent = 'Wszyscy dostawcy';
|
||||||
|
supplierFilter.appendChild(allOption);
|
||||||
|
|
||||||
|
// Opcje dla każdego dostawcy
|
||||||
|
Array.from(suppliers).sort().forEach(supplier => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = supplier;
|
||||||
|
option.textContent = supplier;
|
||||||
|
supplierFilter.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener dla filtra dostawców
|
||||||
|
supplierFilter.addEventListener('change', () => {
|
||||||
|
filterBySupplier(supplierFilter.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
filterContainer.appendChild(label);
|
||||||
|
filterContainer.appendChild(supplierFilter);
|
||||||
|
|
||||||
|
// Wstaw filtr za przyciskami w lewym panelu
|
||||||
|
leftPanel.parentNode.appendChild(filterContainer);
|
||||||
|
|
||||||
|
log('Dodano filtr dostawców za przyciskami w lewym panelu');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback - dodaje filtry przed tabelą jeśli nie ma panelu wyszukiwania
|
||||||
|
*/
|
||||||
|
function addQuickFiltersBeforeTable() {
|
||||||
|
const table = document.querySelector('.o_list_table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
// Kontener dla filtrów (stary styl)
|
||||||
|
const filterContainer = document.createElement('div');
|
||||||
|
filterContainer.className = 'kfz-filter-container';
|
||||||
|
filterContainer.style.cssText = `
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Filtr po dostawcy
|
||||||
|
const supplierFilter = document.createElement('select');
|
||||||
|
supplierFilter.style.cssText = 'padding: 5px; border-radius: 3px; border: 1px solid #ccc;';
|
||||||
|
supplierFilter.innerHTML = '<option value="">Wszyscy dostawcy</option>';
|
||||||
|
|
||||||
|
// Pobierz unikalnych dostawców
|
||||||
|
const suppliers = new Set();
|
||||||
|
const rows = document.querySelectorAll('.o_data_row');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const supplierCell = row.children[3];
|
||||||
|
if (supplierCell) {
|
||||||
|
const supplierName = supplierCell.textContent.trim();
|
||||||
|
if (supplierName) {
|
||||||
|
suppliers.add(supplierName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
suppliers.forEach(supplier => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = supplier;
|
||||||
|
option.textContent = supplier;
|
||||||
|
supplierFilter.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener dla filtra dostawców
|
||||||
|
supplierFilter.addEventListener('change', () => {
|
||||||
|
filterBySupplier(supplierFilter.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
filterContainer.appendChild(document.createTextNode('Filtruj: '));
|
||||||
|
filterContainer.appendChild(supplierFilter);
|
||||||
|
|
||||||
|
// Wstaw filtry przed tabelą
|
||||||
|
table.parentNode.insertBefore(filterContainer, table);
|
||||||
|
|
||||||
|
log('Dodano filtry przed tabelą (fallback)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtruje wiersze według dostawcy
|
||||||
|
* @param {string} supplier - Nazwa dostawcy
|
||||||
|
*/
|
||||||
|
function filterBySupplier(supplier) {
|
||||||
|
const rows = document.querySelectorAll('.o_data_row');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const supplierCell = row.children[3]; // Dostawca jest w komórce 3
|
||||||
|
if (!supplierCell) return;
|
||||||
|
|
||||||
|
const supplierName = supplierCell.textContent.trim();
|
||||||
|
const shouldShow = !supplier || supplierName === supplier;
|
||||||
|
row.style.display = shouldShow ? '' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`Filtrowanie według dostawcy: ${supplier || 'wszyscy'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kopiuje tekst do schowka (jak w idea-orders.user.js)
|
||||||
|
* @param {string} text - Tekst do skopiowania
|
||||||
|
*/
|
||||||
|
async function tmCopyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback dla starszych przeglądarek
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Czyni komórkę kopiowalną (styl z idea-orders.user.js)
|
||||||
|
* @param {Element} td - Komórka tabeli
|
||||||
|
*/
|
||||||
|
function makeCellCopyable(td) {
|
||||||
|
if (!td || td.dataset.tmCopyable) return;
|
||||||
|
td.dataset.tmCopyable = '1';
|
||||||
|
td.classList.add('tm-copyable');
|
||||||
|
td.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
|
||||||
|
let val = (td.textContent || '').trim();
|
||||||
|
if (!val) return;
|
||||||
|
|
||||||
|
// Dla kwot usuń "zł" i zbędne spacje oraz znaki specjalne
|
||||||
|
if (val.includes('zł')) {
|
||||||
|
val = val.replace(/\s*zł\s*$/, '').trim();
|
||||||
|
// Usuń też znaki niełamliwych spacji i inne znaki formatujące
|
||||||
|
val = val.replace(/[\u00A0\u202F\u2009]/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tmCopyToClipboard(val);
|
||||||
|
const old = td.title;
|
||||||
|
td.title = `Skopiowano: ${val}`;
|
||||||
|
td.classList.add('tm-copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
td.title = old || 'Skopiowano';
|
||||||
|
td.classList.remove('tm-copied');
|
||||||
|
}, 1200);
|
||||||
|
log(`Skopiowano: ${val}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Tampermonkey] Błąd podczas kopiowania komórki:', error);
|
||||||
|
td.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true); // capture=true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dodaje funkcjonalność kopiowania do wybranych kolumn
|
||||||
|
*/
|
||||||
|
function addCopyFunctionality() {
|
||||||
|
// Znajdź wszystkie wiersze danych
|
||||||
|
const dataRows = document.querySelectorAll('.o_data_row');
|
||||||
|
log(`Znaleziono ${dataRows.length} wierszy danych`);
|
||||||
|
|
||||||
|
if (dataRows.length === 0) {
|
||||||
|
log('Brak wierszy danych - sprawdzam strukturę tabeli');
|
||||||
|
const allRows = document.querySelectorAll('tr');
|
||||||
|
log(`Wszystkich wierszy: ${allRows.length}`);
|
||||||
|
allRows.forEach((row, index) => {
|
||||||
|
if (index < 5) { // Pokaż tylko pierwsze 5
|
||||||
|
log(`Wiersz ${index}: klasy="${row.className}", komórek=${row.children.length}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dataRows.forEach((row, rowIndex) => {
|
||||||
|
log(`Przetwarzam wiersz ${rowIndex}`);
|
||||||
|
|
||||||
|
// Debuguj wszystkie komórki w wierszu (tylko pierwszy wiersz żeby nie spamować)
|
||||||
|
if (rowIndex === 0) {
|
||||||
|
log(`=== DEBUGOWANIE STRUKTURY PIERWSZEGO WIERSZA ===`);
|
||||||
|
Array.from(row.children).forEach((cell, cellIndex) => {
|
||||||
|
const dataName = cell.getAttribute('data-name');
|
||||||
|
const text = cell.textContent.trim();
|
||||||
|
const classes = cell.className;
|
||||||
|
log(` Komórka ${cellIndex}: data-name="${dataName}", klasy="${classes}", tekst="${text.substring(0, 40)}"`);
|
||||||
|
});
|
||||||
|
log(`=== KONIEC DEBUGOWANIA STRUKTURY ===`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numer faktury - komórka 2 (na podstawie debugowania)
|
||||||
|
let numberCell = row.children[2];
|
||||||
|
|
||||||
|
if (numberCell && numberCell.textContent.includes('KFZ/')) {
|
||||||
|
makeCellCopyable(numberCell);
|
||||||
|
log(`✓ Dodano kopiowanie dla numeru: "${numberCell.textContent.trim()}"`);
|
||||||
|
} else if (numberCell) {
|
||||||
|
log(`Pominięto kopiowanie numeru: "${numberCell.textContent.trim()}" (brak KFZ/)`);
|
||||||
|
} else {
|
||||||
|
log(`Nie znaleziono komórki numeru (indeks 2)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data faktury - komórka 4 (na podstawie debugowania)
|
||||||
|
let dateCell = row.children[4];
|
||||||
|
|
||||||
|
if (dateCell && dateCell.textContent.match(/\d{2}\.\d{2}\.\d{4}/)) {
|
||||||
|
makeCellCopyable(dateCell);
|
||||||
|
log(`✓ Dodano kopiowanie dla daty: "${dateCell.textContent.trim()}"`);
|
||||||
|
} else if (dateCell) {
|
||||||
|
log(`Pominięto kopiowanie daty: "${dateCell.textContent.trim()}" (nie pasuje format)`);
|
||||||
|
} else {
|
||||||
|
log(`Nie znaleziono komórki daty (indeks 4)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dokument źródłowy - komórka 5 (na podstawie debugowania: "ZZ/00310")
|
||||||
|
let invoiceOriginCell = row.children[5];
|
||||||
|
|
||||||
|
if (invoiceOriginCell) {
|
||||||
|
const invoiceOriginText = invoiceOriginCell.textContent.trim();
|
||||||
|
log(`Znaleziono dokument źródłowy: "${invoiceOriginText}"`);
|
||||||
|
if (invoiceOriginText && invoiceOriginText !== '' && invoiceOriginText !== '-') {
|
||||||
|
makeCellCopyable(invoiceOriginCell);
|
||||||
|
log(`✓ Dodano kopiowanie dla dokumentu źródłowego: "${invoiceOriginText}"`);
|
||||||
|
} else {
|
||||||
|
log(`Dokument źródłowy pusty lub myślnik: "${invoiceOriginText}"`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`❌ Nie znaleziono komórki dokumentu źródłowego (indeks 5)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Odnośnik - komórka 6 (na podstawie debugowania: "KFAS/2025/10/269/PL")
|
||||||
|
let refCell = row.children[6];
|
||||||
|
|
||||||
|
if (refCell) {
|
||||||
|
const refText = refCell.textContent.trim();
|
||||||
|
log(`Znaleziono odnośnik: "${refText}"`);
|
||||||
|
if (refText && refText !== '' && refText !== '-') {
|
||||||
|
makeCellCopyable(refCell);
|
||||||
|
log(`✓ Dodano kopiowanie dla odnośnika: "${refText}"`);
|
||||||
|
} else {
|
||||||
|
log(`Odnośnik pusty lub myślnik: "${refText}"`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`❌ Nie znaleziono komórki odnośnika (indeks 6)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Należność - komórka 10 (na podstawie debugowania: "1 123,80 zł")
|
||||||
|
let amountCell = row.children[10];
|
||||||
|
|
||||||
|
if (amountCell) {
|
||||||
|
const amountText = amountCell.textContent.trim();
|
||||||
|
log(`Znaleziono należność: "${amountText}"`);
|
||||||
|
|
||||||
|
if (amountText.includes('zł')) {
|
||||||
|
makeCellCopyable(amountCell);
|
||||||
|
log(`✓ Dodano kopiowanie dla należności: "${amountText}"`);
|
||||||
|
} else {
|
||||||
|
log(`Należność bez 'zł': "${amountText}"`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`❌ Nie znaleziono komórki należności (indeks 10)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Na podstawie - komórka 11 (na podstawie debugowania: "KFZ/283/09/2025 (KFAS/2025/09/799/PL)")
|
||||||
|
let basisCell = row.children[11];
|
||||||
|
|
||||||
|
if (basisCell) {
|
||||||
|
const basisText = basisCell.textContent.trim();
|
||||||
|
log(`Znaleziono "Na podstawie": "${basisText}"`);
|
||||||
|
if (basisText && basisText !== '' && basisText !== '-') {
|
||||||
|
makeCellCopyable(basisCell);
|
||||||
|
log(`✓ Dodano kopiowanie dla "Na podstawie": "${basisText}"`);
|
||||||
|
} else {
|
||||||
|
log(`Na podstawie puste lub myślnik: "${basisText}"`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`❌ Nie znaleziono komórki "Na podstawie" (indeks 11)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log('Zakończono dodawanie funkcjonalności kopiowania');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dodaje style CSS dla funkcjonalności kopiowania (dokładnie jak w idea-orders.user.js)
|
||||||
|
*/
|
||||||
|
function addCopyStyles() {
|
||||||
|
// Sprawdź czy style już zostały dodane
|
||||||
|
if (document.querySelector('#kfz-copy-styles')) {
|
||||||
|
log('Style kopiowania już istnieją - pomijam dodawanie');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'kfz-copy-styles';
|
||||||
|
style.textContent = `
|
||||||
|
/* Kopiowanie – ikona tuż za liczbą (inline), mniejsza, pomarańczowa */
|
||||||
|
td.tm-copyable { cursor: copy; }
|
||||||
|
td.tm-copyable::after {
|
||||||
|
content: '⧉'; /* symbol kopiowania w kolorze CSS */
|
||||||
|
display: inline-block; /* w tej samej linii co liczba */
|
||||||
|
margin-left: 6px; /* mały odstęp od liczby */
|
||||||
|
font-size: 12px; /* mniejszy rozmiar */
|
||||||
|
line-height: 1;
|
||||||
|
color: #ff9800; /* pomarańczowy */
|
||||||
|
opacity: .75; /* widoczna także bez hovera */
|
||||||
|
vertical-align: baseline;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .15s ease;
|
||||||
|
}
|
||||||
|
td.tm-copyable:hover::after { opacity: 1; }
|
||||||
|
td.tm-copyable.tm-copied {
|
||||||
|
outline: 2px solid #ff9800;
|
||||||
|
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Czeka na załadowanie elementu
|
||||||
|
* @param {string} selector - Selektor CSS
|
||||||
|
* @param {number} timeout - Timeout w ms
|
||||||
|
* @returns {Promise<Element>}
|
||||||
|
*/
|
||||||
|
function waitForElement(selector, timeout = 5000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
resolve(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
observer.disconnect();
|
||||||
|
resolve(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
observer.disconnect();
|
||||||
|
reject(new Error(`Element ${selector} nie został znaleziony w czasie ${timeout}ms`));
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Przechwytywanie żądań AJAX do search_read
|
||||||
|
function interceptAjaxRequests() {
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
window.fetch = function(...args) {
|
||||||
|
const promise = originalFetch.apply(this, args);
|
||||||
|
|
||||||
|
// Sprawdź czy to żądanie do search_read
|
||||||
|
if (args[0] && args[0].includes('/web/dataset/search_read')) {
|
||||||
|
promise.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
log('Wykryto żądanie AJAX search_read - uruchamiam skrypt po opóźnieniu');
|
||||||
|
setTimeout(initEnhancements, 1500); // Większe opóźnienie dla AJAX
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Przechwytywanie XMLHttpRequest (backup)
|
||||||
|
const originalXHR = window.XMLHttpRequest.prototype.open;
|
||||||
|
window.XMLHttpRequest.prototype.open = function(method, url, ...args) {
|
||||||
|
if (url && url.includes('/web/dataset/search_read')) {
|
||||||
|
this.addEventListener('load', () => {
|
||||||
|
log('Wykryto XHR search_read - uruchamiam skrypt po opóźnieniu');
|
||||||
|
setTimeout(initEnhancements, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return originalXHR.call(this, method, url, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicjalizacja skryptu z obserwacją zmian
|
||||||
|
function startScript() {
|
||||||
|
log('🚀 SKRYPT ZAŁADOWANY - WERSJA 3.0');
|
||||||
|
log(`📍 URL: ${window.location.href}`);
|
||||||
|
|
||||||
|
// Przechwytuj żądania AJAX
|
||||||
|
interceptAjaxRequests();
|
||||||
|
|
||||||
|
// Pierwsze uruchomienie
|
||||||
|
setTimeout(initEnhancements, 1000);
|
||||||
|
|
||||||
|
// Obserwuj zmiany w DOM (dla dynamicznych stron)
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
// Sprawdź czy dodano nowe wiersze danych
|
||||||
|
const hasNewDataRows = mutations.some(mutation =>
|
||||||
|
Array.from(mutation.addedNodes).some(node =>
|
||||||
|
node.nodeType === 1 && (
|
||||||
|
node.classList?.contains('o_data_row') ||
|
||||||
|
node.querySelector?.('.o_data_row')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasNewDataRows) {
|
||||||
|
log('Wykryto nowe wiersze danych - uruchamiam skrypt');
|
||||||
|
setTimeout(initEnhancements, 800);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obserwuj zmiany URL (hash) - kilka metod dla pewności
|
||||||
|
let lastUrl = window.location.href;
|
||||||
|
|
||||||
|
// 1. Standardowy hashchange
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
log('Wykryto hashchange - resetuję stan i uruchamiam skrypt');
|
||||||
|
// Resetuj stan przy zmianie URL
|
||||||
|
const scriptMarker = document.querySelector('.kfz-script-initialized');
|
||||||
|
if (scriptMarker) {
|
||||||
|
scriptMarker.remove();
|
||||||
|
log('Usunięto marker inicjalizacji przy hashchange');
|
||||||
|
}
|
||||||
|
setTimeout(initEnhancements, 500);
|
||||||
|
setTimeout(initEnhancements, 1500); // Backup po dłuższym czasie
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Obserwuj zmiany URL przez polling
|
||||||
|
setInterval(() => {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
if (currentUrl !== lastUrl) {
|
||||||
|
log(`URL zmieniony z ${lastUrl} na ${currentUrl}`);
|
||||||
|
lastUrl = currentUrl;
|
||||||
|
// Resetuj stan przy zmianie URL
|
||||||
|
const scriptMarker = document.querySelector('.kfz-script-initialized');
|
||||||
|
if (scriptMarker) {
|
||||||
|
scriptMarker.remove();
|
||||||
|
log('Usunięto marker inicjalizacji przy zmianie URL');
|
||||||
|
}
|
||||||
|
setTimeout(initEnhancements, 300);
|
||||||
|
setTimeout(initEnhancements, 1000);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 3. Obserwuj zmiany w tytule strony (Odoo często zmienia tytuł)
|
||||||
|
let lastTitle = document.title;
|
||||||
|
const titleObserver = new MutationObserver(() => {
|
||||||
|
if (document.title !== lastTitle) {
|
||||||
|
log(`Tytuł zmieniony z "${lastTitle}" na "${document.title}"`);
|
||||||
|
lastTitle = document.title;
|
||||||
|
setTimeout(initEnhancements, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
titleObserver.observe(document.querySelector('title') || document.head, {
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Obserwuj zmiany w breadcrumb (ścieżka nawigacji Odoo)
|
||||||
|
const breadcrumbObserver = new MutationObserver(() => {
|
||||||
|
log('Wykryto zmiany w breadcrumb - sprawdzam czy to strona KFZ');
|
||||||
|
setTimeout(initEnhancements, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obserwuj breadcrumb jeśli istnieje
|
||||||
|
const breadcrumbElement = document.querySelector('.breadcrumb, .o_cp_controller');
|
||||||
|
if (breadcrumbElement) {
|
||||||
|
breadcrumbObserver.observe(breadcrumbElement, {
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Dodatkowe sprawdzenie co 3 sekundy (backup)
|
||||||
|
setInterval(() => {
|
||||||
|
const url = window.location.href;
|
||||||
|
const isAccountMoveList = url.includes('model=account.move') && url.includes('view_type=list');
|
||||||
|
const hasFilter = document.querySelector('.kfz-filter-container');
|
||||||
|
const breadcrumbCheck = document.querySelector('.breadcrumb');
|
||||||
|
const hasCorrectionsBreadcrumb = breadcrumbCheck && breadcrumbCheck.textContent.includes('Korekty');
|
||||||
|
|
||||||
|
if (isAccountMoveList && !hasFilter && (isKfzListingPage() || hasCorrectionsBreadcrumb)) {
|
||||||
|
log('Backup check - wykryto stronę account.move bez filtra, uruchamiam skrypt');
|
||||||
|
initEnhancements();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', startScript);
|
||||||
|
} else {
|
||||||
|
startScript();
|
||||||
|
}
|
||||||
|
})();
|
||||||
583
idea/idea-kfz-zd.user.js
Normal file
583
idea/idea-kfz-zd.user.js
Normal file
|
|
@ -0,0 +1,583 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name IDEA ERP – KFZ
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 0.16
|
||||||
|
// @description Przełączanie widoczności produktów z korektą w module KFZ IDEA ERP
|
||||||
|
// @author Adam
|
||||||
|
// @match https://emma.ideaerp.pl/*
|
||||||
|
// @icon https://emma.ideaerp.pl/web/static/src/img/favicon.ico
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/kfz-zd
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/kfz-zd
|
||||||
|
// @grant none
|
||||||
|
// @run-at document-end
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[Tampermonkey KFZ] Skrypt załadowany - wersja 0.16');
|
||||||
|
} catch(e) {
|
||||||
|
alert('Błąd w Tampermonkey KFZ: ' + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================OPIS==============================
|
||||||
|
WERSJA 0.16 - Znajdowanie kolumny po indeksie (data-name tylko w th, nie w td)
|
||||||
|
|
||||||
|
WYMAGANE PARAMETRY URL:
|
||||||
|
- action=276 (ID akcji KFZ - Korekty zakupu)
|
||||||
|
- menu_id=141 (ID menu KFZ)
|
||||||
|
- view_type=form (tylko widok formularza)
|
||||||
|
- Skrypt działa TYLKO na stronie faktury korygującej (KFZ)
|
||||||
|
|
||||||
|
WYMAGANE ELEMENTY:
|
||||||
|
- Element .o_notebook ul.nav.nav-tabs (zakładki na stronie)
|
||||||
|
- Tabela .o_field_x2many[name="invoice_line_ids"] table.o_list_table
|
||||||
|
- Kolumna data-name="price_subtotal" (wartość korekty)
|
||||||
|
- Element .o_pager .o_pager_value (zakres paginacji)
|
||||||
|
- Element .o_pager .o_pager_limit (całkowita liczba produktów)
|
||||||
|
|
||||||
|
DZIAŁANIE SKRYPTU:
|
||||||
|
- Dodaje przycisk w zakładkach strony
|
||||||
|
- "Pokaż korygowane produkty" → ładuje wszystkie produkty i ukrywa wiersze z wartością = 0
|
||||||
|
- "Pokaż wszystko" → pokazuje wszystkie wiersze i resetuje do domyślnego widoku
|
||||||
|
- Filtrowanie po kolumnie price_subtotal (nie używa indeksów kolumn)
|
||||||
|
- Bezpieczne obsługiwanie dla < 41 produktów (brak paginacji)
|
||||||
|
|
||||||
|
FUNKCJE:
|
||||||
|
- runScriptCheck() - sprawdza czy hash się zmienił i czy to właściwa strona, dodaje przycisk tylko raz
|
||||||
|
- isCorrectPage() - sprawdza czy to właściwa strona KFZ (action=276, menu_id=141, view_type=form)
|
||||||
|
- createTestButton() - tworzy przycisk w interfejsie
|
||||||
|
- parsePolishNumber() - parsuje polskie wartości numeryczne (przecinek, 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 <td>
|
||||||
|
- Teraz znajdujemy kolumnę po indeksie z nagłówka <th[data-name="price_subtotal"]>
|
||||||
|
- Pobieramy indeks kolumny z nagłówka i używamy go do znalezienia komórki w wierszu
|
||||||
|
- Dodano obsługę wielu wierszy w thead (pomijamy .i7_list_search_wrap)
|
||||||
|
- Dodano szczegółowe logi pokazujące liczbę znalezionych komórek w wierszu
|
||||||
|
- Podwyższenie numeru wersji do 0.16
|
||||||
|
|
||||||
|
v0.15 (2025) - POPRAWIONO PARSOWANIE WARTOŚCI Z &NBSP; I PRZECINKAMI
|
||||||
|
- Naprawiono funkcję parsePolishNumber() - zamiana WSZYSTKICH przecinków na kropki
|
||||||
|
- Dodano obsługę &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 <th>, więc indeks się zgadza
|
||||||
|
const valueCell = cells[priceSubtotalIndex];
|
||||||
|
|
||||||
|
if (!valueCell) {
|
||||||
|
console.log('[Tampermonkey KFZ] Wiersz', index + 1, '- brak komórki na indeksie', priceSubtotalIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Tampermonkey KFZ] Wiersz', index + 1, '- komórka:', valueCell);
|
||||||
|
|
||||||
|
// Pobieramy wartość z atrybutu title lub textContent
|
||||||
|
const valueText = valueCell.getAttribute('title') || valueCell.textContent;
|
||||||
|
const numericValue = parsePolishNumber(valueText);
|
||||||
|
|
||||||
|
console.log('[Tampermonkey KFZ] Wiersz', index + 1, ':', valueText, '->', numericValue);
|
||||||
|
|
||||||
|
if (showOnlyWithValue) {
|
||||||
|
// Pokaż tylko wiersze z wartością ≠ 0
|
||||||
|
if (numericValue !== 0) {
|
||||||
|
row.style.display = '';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
hiddenCount++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pokaż wszystkie wiersze
|
||||||
|
row.style.display = '';
|
||||||
|
visibleCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[Tampermonkey KFZ] Filtrowanie zakończone - widoczne:', visibleCount, 'ukryte:', hiddenCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funkcja do ładowania wszystkich produktów
|
||||||
|
function loadAllProducts() {
|
||||||
|
console.log('[Tampermonkey KFZ] Ładuję wszystkie produkty...');
|
||||||
|
|
||||||
|
// Szukamy elementów paginacji - trzeba znaleźć je w kontekście tabeli pozycji faktury
|
||||||
|
const pager = document.querySelector('.o_field_x2many[name="invoice_line_ids"] .o_pager');
|
||||||
|
|
||||||
|
// Jeśli nie ma paginacji, to znaczy że jest mniej niż 41 produktów - wszystkie są już widoczne
|
||||||
|
if (!pager) {
|
||||||
|
console.log('[Tampermonkey KFZ] Nie ma paginacji - wszystkie produkty (< 41) są już widoczne');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagerLimit = pager.querySelector('.o_pager_limit');
|
||||||
|
const pagerValue = pager.querySelector('.o_pager_value');
|
||||||
|
|
||||||
|
if (!pagerLimit || !pagerValue) {
|
||||||
|
console.log('[Tampermonkey KFZ] Nie znaleziono elementów paginacji');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalProducts = pagerLimit.textContent.trim();
|
||||||
|
const currentRange = pagerValue.textContent.trim();
|
||||||
|
|
||||||
|
console.log('[Tampermonkey KFZ] Aktualny zakres:', currentRange, 'Całkowita liczba:', totalProducts);
|
||||||
|
|
||||||
|
// Jeśli wartości są puste, to znaczy że nie ma prawidłowej paginacji
|
||||||
|
if (!totalProducts || !currentRange) {
|
||||||
|
console.log('[Tampermonkey KFZ] Wartości paginacji są puste - wszystkie produkty są już widoczne');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprawdzamy, czy już są pokazane wszystkie produkty
|
||||||
|
if (currentRange.includes(`1-${totalProducts}`)) {
|
||||||
|
console.log('[Tampermonkey KFZ] Wszystkie produkty są już załadowane');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Tampermonkey KFZ] Zmieniam zakres z', currentRange, 'na 1-', totalProducts);
|
||||||
|
changePaginationRange(`1-${totalProducts}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funkcja do resetowania do domyślnego widoku
|
||||||
|
function resetToDefault() {
|
||||||
|
console.log('[Tampermonkey KFZ] Resetuję do domyślnego widoku...');
|
||||||
|
|
||||||
|
// Sprawdzamy czy w ogóle jest paginacja
|
||||||
|
const pager = document.querySelector('.o_field_x2many[name="invoice_line_ids"] .o_pager');
|
||||||
|
|
||||||
|
if (!pager) {
|
||||||
|
console.log('[Tampermonkey KFZ] Nie ma paginacji (< 41 produktów) - reset nie jest potrzebny');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changePaginationRange('1-40');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestButton() {
|
||||||
|
// Szukamy przycisku w zakładkach
|
||||||
|
const nav = document.querySelector('.o_notebook ul.nav.nav-tabs');
|
||||||
|
if (!nav) {
|
||||||
|
console.log('[Tampermonkey KFZ] Nie znaleziono nav-tabs');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Tampermonkey KFZ] Znaleziono nav-tabs, tworzę przycisk');
|
||||||
|
|
||||||
|
// Tworzymy element li
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'nav-item';
|
||||||
|
|
||||||
|
// Tworzymy link
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = '#';
|
||||||
|
a.className = 'nav-link';
|
||||||
|
a.textContent = 'Pokaż korygowane produkty';
|
||||||
|
|
||||||
|
// Dodajemy event listener
|
||||||
|
a.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Sprawdzamy aktualną nazwę
|
||||||
|
if (a.textContent === 'Pokaż korygowane produkty') {
|
||||||
|
a.textContent = 'Pokaż wszystko';
|
||||||
|
console.log('[Tampermonkey KFZ] Zmieniono nazwę na: Pokaż wszystko');
|
||||||
|
|
||||||
|
// Ładujemy wszystkie produkty
|
||||||
|
loadAllProducts();
|
||||||
|
|
||||||
|
// Po załadowaniu filtrujemy wiersze (ukrywamy wartość = 0)
|
||||||
|
setTimeout(() => {
|
||||||
|
toggleValueRows(true);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
a.textContent = 'Pokaż korygowane produkty';
|
||||||
|
console.log('[Tampermonkey KFZ] Zmieniono nazwę na: Pokaż korygowane produkty');
|
||||||
|
|
||||||
|
// Pokaż wszystkie wiersze
|
||||||
|
toggleValueRows(false);
|
||||||
|
|
||||||
|
// Resetujemy do domyślnego widoku
|
||||||
|
resetToDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
li.appendChild(a);
|
||||||
|
nav.appendChild(li);
|
||||||
|
|
||||||
|
console.log('[Tampermonkey KFZ] Przycisk dodany pomyślnie');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funkcja inicjalizująca
|
||||||
|
function init() {
|
||||||
|
console.log('[Tampermonkey KFZ] Inicjalizacja...');
|
||||||
|
|
||||||
|
// W aplikacji SPA parametry URL mogą być ustawiane asynchronicznie
|
||||||
|
// Czekamy do 10 sekund na poprawne parametry URL
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 100; // 100 * 100ms = 10 sekund
|
||||||
|
|
||||||
|
console.log('[Tampermonkey KFZ] Rozpoczynam sprawdzanie URL (max 100 prób co 100ms)');
|
||||||
|
|
||||||
|
// Nasłuchujemy na zmiany URL (hashchange)
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
console.log('[Tampermonkey KFZ] 🎯 Hash changed!');
|
||||||
|
buttonAdded = false; // Resetujemy stan - możliwa nowa strona
|
||||||
|
lastHash = ''; // Resetujemy hash żeby wykryć zmianę
|
||||||
|
setTimeout(runScriptCheck, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlCheckInterval = setInterval(() => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
// Log co 10 prób
|
||||||
|
if (attempts % 10 === 0 || attempts === 1) {
|
||||||
|
console.log('[Tampermonkey KFZ] Próba', attempts, '/', maxAttempts, 'sprawdzenia URL...');
|
||||||
|
}
|
||||||
|
|
||||||
|
runScriptCheck();
|
||||||
|
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
console.log('[Tampermonkey KFZ] ❌ Timeout po', attempts, 'próbach');
|
||||||
|
clearInterval(urlCheckInterval);
|
||||||
|
}
|
||||||
|
}, 100); // Sprawdzamy co 100ms
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funkcja sprawdzająca elementy strony po potwierdzeniu URL
|
||||||
|
function startElementCheck() {
|
||||||
|
// Szukamy elementów strony
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
const nav = document.querySelector('.o_notebook ul.nav.nav-tabs');
|
||||||
|
console.log('[Tampermonkey KFZ] Sprawdzam nav-tabs:', !!nav);
|
||||||
|
|
||||||
|
if (nav) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
createTestButton();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Timeout po 10 sekundach
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
console.log('[Tampermonkey KFZ] Timeout - nie znaleziono nav-tabs');
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
784
idea/idea-kfz.user.js
Normal file
784
idea/idea-kfz.user.js
Normal file
|
|
@ -0,0 +1,784 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name IDEAERP - KFZ Korekty
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.8
|
||||||
|
// @description Kopiowanie danych z tabeli KFZ, pól formularza i podsumowania - kliknij aby skopiować
|
||||||
|
// @match https://emma.ideaerp.pl/web*
|
||||||
|
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/kfz
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/kfz
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_deleteValue
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// @run-at document-end
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ========================OPIS==============================
|
||||||
|
WERSJA 1.1 - Funkcjonalność kopiowania danych z tabeli KFZ, pól formularza i podsumowania
|
||||||
|
|
||||||
|
WYMAGANE PARAMETRY URL:
|
||||||
|
- action=278 → ID akcji KFZ
|
||||||
|
- menu_id=141 → ID menu KFZ
|
||||||
|
- model=account.move → Model faktury/dokumentu
|
||||||
|
- view_type=form → Widok formularza
|
||||||
|
|
||||||
|
WYMAGANE KOLUMNY w tabeli KFZ:
|
||||||
|
- product_id → Produkt
|
||||||
|
- quantity_after → Ilość po korekcie
|
||||||
|
- price_unit_after → Cena jednostkowa po korekcie
|
||||||
|
- price_subtotal → Wartość
|
||||||
|
- invoice_line_net → Netto
|
||||||
|
- invoice_line_tax Podatek
|
||||||
|
- invoice_line_gross → Brutto
|
||||||
|
|
||||||
|
DZIAŁANIE SKRYPTU:
|
||||||
|
- Kopiowanie produktów z trybem SKU/pełna nazwa (jak w idea-orders)
|
||||||
|
- Kopiowanie wartości liczbowych z kolumn KFZ
|
||||||
|
- Ikona kopiowania ⧉ przy każdej kopiowalnej komórce
|
||||||
|
- Przycisk przełączania trybu kopiowania produktów
|
||||||
|
- Obsługa błędów zgodnie z zasadami .rules
|
||||||
|
- Automatyczne wykrywanie kolumn po atrybucie title
|
||||||
|
|
||||||
|
FUNKCJE KOPIOWANIA:
|
||||||
|
- Produkt: SKU (z nawiasów kwadratowych) lub pełna nazwa
|
||||||
|
- Ilość po korekcie: wartość liczbowa
|
||||||
|
- Cena jednostkowa po korekcie: wartość liczbowa
|
||||||
|
- Wartość: wartość liczbowa
|
||||||
|
- Netto: wartość liczbowa
|
||||||
|
- Podatek: wartość liczbowa
|
||||||
|
- Brutto: wartość liczbowa
|
||||||
|
|
||||||
|
ZAPAMIĘTYWANIE USTAWIEŃ:
|
||||||
|
- Tryb kopiowania produktów (SKU/pełna nazwa) zapisywany w pamięci
|
||||||
|
- Ustawienia przetrwają restart przeglądarki i odświeżenie strony
|
||||||
|
|
||||||
|
AUTOMATYCZNE AKTUALIZACJE:
|
||||||
|
- Serwer aktualizacji: https://n8n.emma.net.pl/webhook/kfz
|
||||||
|
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
|
||||||
|
|
||||||
|
CHANGELOG:
|
||||||
|
v1.8 (2024) - PRZYWRÓCONO DWUKROPEK W TEKŚCIE PRZYCISKU
|
||||||
|
- Przywrócono dwukropek po słowie "Kopiuję"
|
||||||
|
- Tekst przycisku: "Kopiuję: SKU" i "Kopiuję: Pełna nazwa"
|
||||||
|
- Podwyższenie numeru wersji do 1.8
|
||||||
|
|
||||||
|
v1.7 (2024) - USUNIĘTO DWUKROPEK Z TEKSTU PRZYCISKU
|
||||||
|
- Zmieniono tekst przycisku z "Kopiuję: SKU" na "Kopiuję SKU"
|
||||||
|
- Zmieniono tekst z "Kopiuję: Pełna nazwa" na "Kopiuję Pełna nazwa"
|
||||||
|
- Usunięto dwukropek dla lepszej czytelności
|
||||||
|
- Podwyższenie numeru wersji do 1.7
|
||||||
|
|
||||||
|
v1.6 (2024) - POPRAWKA LOKALIZACJI PRZYCISKU W TAB-CONTENT
|
||||||
|
- Poprawiono findActionsContainer() - szuka w .tab-content > .tab-pane
|
||||||
|
- Przycisk umieszczany w zakładce obok "Załaduj z cennika" i "Masowa zmiana"
|
||||||
|
- Dodano wyszukiwanie zakładek z przyciskami (load_from_pricelist, button[name="523"])
|
||||||
|
- Przycisk w właściwym miejscu: <div class="tab-content"> zamiast o_control_panel
|
||||||
|
- Podwyższenie numeru wersji do 1.6
|
||||||
|
|
||||||
|
v1.5 (2024) - POPRAWKA UMIESZCZANIA PRZYCISKU
|
||||||
|
- Poprawiono funkcję findActionsContainer() - priorytet dla .o_form_buttons_view
|
||||||
|
- Przycisk "Kopiuję: SKU" teraz umieszczany w tym samym miejscu co w idea-orders
|
||||||
|
- Usunięto skomplikowaną logikę umieszczania w zakładkach
|
||||||
|
- Dokładnie ta sama strategia umieszczania co w idea-orders.user.js
|
||||||
|
- Podwyższenie numeru wersji do 1.5
|
||||||
|
|
||||||
|
v1.4 (2024) - DODANO BRAKUJĄCE POLA
|
||||||
|
- Dodano kopiowanie nazwy dokumentu (name) - np. "KFZ/302/10/2025"
|
||||||
|
- Dodano kopiowanie NIP (invoice_name) - np. "CSG SPÓŁKA AKCYJNA"
|
||||||
|
- Poprawiono mapowanie pól formularza na rzeczywiste selektory HTML
|
||||||
|
- Rozszerzone selektory dla pola name (również h1 span[name="name"])
|
||||||
|
- Podwyższenie numeru wersji do 1.4
|
||||||
|
|
||||||
|
v1.3 (2024) - NAPRAWKA BŁĘDU SELEKTORÓW CSS
|
||||||
|
- Usunięto nieprawidłowe selektory :contains() powodujące błąd SyntaxError
|
||||||
|
- Dodano bezpieczne try/catch dla wszystkich selektorów
|
||||||
|
- Alternatywne szukanie pól po etykietach bez :contains()
|
||||||
|
- Naprawiono blokowanie strony przez nieprawidłowe selektory
|
||||||
|
- Podwyższenie numeru wersji do 1.3
|
||||||
|
|
||||||
|
v1.2 (2024) - POPRAWKI KOPIOWANIA VAT I PÓL FORMULARZA
|
||||||
|
- Poprawiono kopiowanie VAT - znaczek kopiowania przy wartościach, nie nazwach
|
||||||
|
- Dodano commercial_partner_id jako "Nazwa na fakturę"
|
||||||
|
- Rozszerzone selektory dla NIP (partner_vat, vat)
|
||||||
|
- Dodano kopiowanie uwag (note, narration)
|
||||||
|
- Lepsze wykrywanie elementów po etykietach
|
||||||
|
- Podwyższenie numeru wersji do 1.2
|
||||||
|
|
||||||
|
v1.1 (2024) - KOPIOWANIE PÓL FORMULARZA I PODSUMOWANIA
|
||||||
|
- Dodano kopiowanie pól formularza: Nazwa na fakturę, NIP, Odnosnik, Data faktury
|
||||||
|
- Dodano kopiowanie pól podsumowania: Należność, Suma, Kwota bez podatku
|
||||||
|
- Dodano kopiowanie VAT (różne stawki)
|
||||||
|
- Automatyczna konwersja wartości ujemnych na dodatnie w podsumowaniu
|
||||||
|
- Funkcja makePositive() dla zapewnienia dodatnich wartości
|
||||||
|
- Rozszerzone selektory dla różnych typów elementów (span, a, input)
|
||||||
|
- Podwyższenie numeru wersji do 1.1
|
||||||
|
|
||||||
|
v1.0 (2024) - PIERWSZA WERSJA
|
||||||
|
- Dodano kopiowanie produktów z trybem SKU/pełna nazwa
|
||||||
|
- Dodano kopiowanie wartości liczbowych z 6 kolumn KFZ
|
||||||
|
- Dodano style CSS dla elementów kopiowalnych (pomarańczowy kolor #ff9800)
|
||||||
|
- Dodano przycisk przełączania trybu kopiowania
|
||||||
|
- Dodano menu Tampermonkey z opcjami konfiguracji
|
||||||
|
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
|
||||||
|
- Automatyczne wykrywanie kolumn po title zamiast indeksów
|
||||||
|
- Spójny styl wizualny z idea-orders.user.js
|
||||||
|
- Precyzyjne wykrywanie strony KFZ po parametrach action=278 i menu_id=141
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Debug flag zgodnie z zasadami .rules
|
||||||
|
const DEBUG = false;
|
||||||
|
const log = (...args) => DEBUG && console.log('[Tampermonkey KFZ]', ...args);
|
||||||
|
|
||||||
|
// ——— Klucze stanu ———
|
||||||
|
const COPY_MODE_KEY = 'tm_kfz_product_copy_mode_v10'; // true = SKU, false = pełna nazwa (domyślnie true)
|
||||||
|
const TABLE_SEL = 'table.o_list_table.o_section_and_note_list_view';
|
||||||
|
|
||||||
|
// Mapowanie title na data-name dla kolumn do kopiowania
|
||||||
|
const COPYABLE_COLUMNS = {
|
||||||
|
'Produkt': 'product_id',
|
||||||
|
'Ilość po korekcie': 'quantity_after',
|
||||||
|
'Cena jednostkowa po korekcie': 'price_unit_after',
|
||||||
|
'Wartość': 'price_subtotal',
|
||||||
|
'Netto': 'invoice_line_net',
|
||||||
|
'Podatek': 'invoice_line_tax',
|
||||||
|
'Brutto': 'invoice_line_gross'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pola formularza do kopiowania
|
||||||
|
const COPYABLE_FORM_FIELDS = {
|
||||||
|
'name': 'Nazwa dokumentu',
|
||||||
|
'commercial_partner_id': 'Nazwa na fakturę',
|
||||||
|
'partner_id': 'Kontrahent',
|
||||||
|
'invoice_name': 'NIP',
|
||||||
|
'partner_vat': 'NIP (alternatywny)',
|
||||||
|
'ref': 'Odnosnik',
|
||||||
|
'invoice_date': 'Data faktury',
|
||||||
|
'note': 'Uwagi',
|
||||||
|
'narration': 'Uwagi wewnętrzne'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pola podsumowania do kopiowania
|
||||||
|
const COPYABLE_SUMMARY_FIELDS = {
|
||||||
|
'amount_residual': 'Należność',
|
||||||
|
'amount_total': 'Suma',
|
||||||
|
'amount_untaxed': 'Kwota bez podatku'
|
||||||
|
};
|
||||||
|
|
||||||
|
GM_addStyle(`
|
||||||
|
/* Kopiowanie – ikona tuż za liczbą (inline), mniejsza, pomarańczowa */
|
||||||
|
td.tm-copyable { cursor: copy; }
|
||||||
|
td.tm-copyable::after {
|
||||||
|
content: '⧉'; /* symbol kopiowania w kolorze CSS */
|
||||||
|
display: inline-block; /* w tej samej linii co liczba */
|
||||||
|
margin-left: 6px; /* mały odstęp od liczby */
|
||||||
|
font-size: 12px; /* mniejszy rozmiar */
|
||||||
|
line-height: 1;
|
||||||
|
color: #ff9800; /* pomarańczowy */
|
||||||
|
opacity: .75; /* widoczna także bez hovera */
|
||||||
|
vertical-align: baseline;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .15s ease;
|
||||||
|
}
|
||||||
|
td.tm-copyable:hover::after { opacity: 1; }
|
||||||
|
td.tm-copyable.tm-copied {
|
||||||
|
outline: 2px solid #ff9800;
|
||||||
|
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// ——— Pomocnicze funkcje ———
|
||||||
|
const textOf = el => el ? (el.getAttribute('title') || el.textContent || '').trim() : '';
|
||||||
|
|
||||||
|
// Funkcja do konwersji wartości na dodatnią
|
||||||
|
const makePositive = (value) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// Usuń znaki waluty i spacje
|
||||||
|
const cleanValue = value.replace(/[^\d,.-]/g, '').replace(',', '.');
|
||||||
|
const numValue = parseFloat(cleanValue);
|
||||||
|
return isNaN(numValue) ? value : Math.abs(numValue).toString();
|
||||||
|
}
|
||||||
|
return Math.abs(parseFloat(value) || 0).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
function isKFZForm() {
|
||||||
|
const params = new URLSearchParams((location.hash || '').replace(/^#/, ''));
|
||||||
|
const action = params.get('action');
|
||||||
|
const menuId = params.get('menu_id');
|
||||||
|
const model = params.get('model');
|
||||||
|
const viewType = params.get('view_type');
|
||||||
|
|
||||||
|
log('Sprawdzanie parametrów URL:', {
|
||||||
|
action, menuId, model, viewType,
|
||||||
|
hash: location.hash,
|
||||||
|
url: location.href
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sprawdź czy to właściwa strona KFZ z konkretnymi parametrami
|
||||||
|
const isKFZ = action === '278' &&
|
||||||
|
menuId === '141' &&
|
||||||
|
model === 'account.move' &&
|
||||||
|
viewType === 'form';
|
||||||
|
|
||||||
|
log('Czy to strona KFZ?', isKFZ);
|
||||||
|
return isKFZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Funkcja wyodrębniania SKU z nawiasów kwadratowych ———
|
||||||
|
function extractSKU(productText) {
|
||||||
|
const match = productText.match(/\[([^\]]+)\]/);
|
||||||
|
return match ? match[1] : productText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Schowek: kopiowanie wartości z komórek ———
|
||||||
|
async function tmCopyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} catch (error) {
|
||||||
|
log('Fallback do document.execCommand dla kopiowania');
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Kopiowanie zwykłych komórek (wartości liczbowe) ———
|
||||||
|
function makeCellCopyable(td, makePositiveValue = false) {
|
||||||
|
if (!td || td.dataset.tmCopyable) return;
|
||||||
|
td.dataset.tmCopyable = '1';
|
||||||
|
td.classList.add('tm-copyable');
|
||||||
|
td.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
|
||||||
|
let val = (td.textContent || '').trim();
|
||||||
|
if (!val) return;
|
||||||
|
|
||||||
|
// Konwertuj na wartość dodatnią jeśli wymagane
|
||||||
|
if (makePositiveValue) {
|
||||||
|
val = makePositive(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tmCopyToClipboard(val);
|
||||||
|
const old = td.title;
|
||||||
|
td.title = `Skopiowano: ${val}`;
|
||||||
|
td.classList.add('tm-copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
td.title = old || 'Skopiowano';
|
||||||
|
td.classList.remove('tm-copied');
|
||||||
|
}, 1200);
|
||||||
|
log('Skopiowano komórkę:', val);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Tampermonkey KFZ] Błąd podczas kopiowania komórki:', error);
|
||||||
|
td.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true); // capture=true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Kopiowanie produktów z trybem SKU/pełna nazwa ———
|
||||||
|
function makeProductCellCopyable(td) {
|
||||||
|
if (!td || td.dataset.tmProductCopyable) return;
|
||||||
|
td.dataset.tmProductCopyable = '1';
|
||||||
|
td.classList.add('tm-copyable');
|
||||||
|
td.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
|
||||||
|
const fullText = (td.textContent || '').trim();
|
||||||
|
if (!fullText) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true); // domyślnie SKU
|
||||||
|
const textToCopy = copyOnlySKU ? extractSKU(fullText) : fullText;
|
||||||
|
|
||||||
|
await tmCopyToClipboard(textToCopy);
|
||||||
|
const old = td.title;
|
||||||
|
const mode = copyOnlySKU ? 'SKU' : 'pełną nazwę';
|
||||||
|
td.title = `Skopiowano ${mode}: ${textToCopy}`;
|
||||||
|
td.classList.add('tm-copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
td.title = old || 'Skopiowano';
|
||||||
|
td.classList.remove('tm-copied');
|
||||||
|
}, 1200);
|
||||||
|
log('Skopiowano produkt:', textToCopy, 'tryb:', mode);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Tampermonkey KFZ] Błąd podczas kopiowania produktu:', error);
|
||||||
|
td.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true); // capture=true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Kopiowanie pól formularza ———
|
||||||
|
function makeFormFieldCopyable(element, fieldName) {
|
||||||
|
if (!element || element.dataset.tmFormCopyable) return;
|
||||||
|
element.dataset.tmFormCopyable = '1';
|
||||||
|
element.classList.add('tm-copyable');
|
||||||
|
element.style.cursor = 'copy';
|
||||||
|
element.title = `Kliknij aby skopiować ${fieldName}`;
|
||||||
|
|
||||||
|
element.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
// Różne sposoby wyciągania tekstu w zależności od typu elementu
|
||||||
|
if (element.tagName === 'SPAN') {
|
||||||
|
text = (element.textContent || '').trim();
|
||||||
|
} else if (element.tagName === 'A') {
|
||||||
|
text = (element.querySelector('span')?.textContent || element.textContent || '').trim();
|
||||||
|
} else {
|
||||||
|
text = (element.textContent || element.value || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tmCopyToClipboard(text);
|
||||||
|
const old = element.title;
|
||||||
|
element.title = `Skopiowano ${fieldName}: ${text}`;
|
||||||
|
element.classList.add('tm-copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
element.title = old || `Kliknij aby skopiować ${fieldName}`;
|
||||||
|
element.classList.remove('tm-copied');
|
||||||
|
}, 1200);
|
||||||
|
log(`Skopiowano ${fieldName}:`, text);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Tampermonkey KFZ] Błąd podczas kopiowania ${fieldName}:`, error);
|
||||||
|
element.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Kopiowanie pól podsumowania (z konwersją na wartości dodatnie) ———
|
||||||
|
function makeSummaryFieldCopyable(element, fieldName) {
|
||||||
|
if (!element || element.dataset.tmSummaryCopyable) return;
|
||||||
|
element.dataset.tmSummaryCopyable = '1';
|
||||||
|
element.classList.add('tm-copyable');
|
||||||
|
element.style.cursor = 'copy';
|
||||||
|
element.title = `Kliknij aby skopiować ${fieldName}`;
|
||||||
|
|
||||||
|
element.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let text = (element.textContent || '').trim();
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
// Konwertuj na wartość dodatnią
|
||||||
|
const positiveValue = makePositive(text);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tmCopyToClipboard(positiveValue);
|
||||||
|
const old = element.title;
|
||||||
|
element.title = `Skopiowano ${fieldName}: ${positiveValue}`;
|
||||||
|
element.classList.add('tm-copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
element.title = old || `Kliknij aby skopiować ${fieldName}`;
|
||||||
|
element.classList.remove('tm-copied');
|
||||||
|
}, 1200);
|
||||||
|
log(`Skopiowano ${fieldName}:`, positiveValue);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Tampermonkey KFZ] Błąd podczas kopiowania ${fieldName}:`, error);
|
||||||
|
element.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Znajdowanie kolumn po title ———
|
||||||
|
function findColumnByTitle(table, title) {
|
||||||
|
const headers = table.querySelectorAll('thead th');
|
||||||
|
log(`Szukanie kolumny "${title}" w ${headers.length} nagłówkach`);
|
||||||
|
|
||||||
|
for (let i = 0; i < headers.length; i++) {
|
||||||
|
const headerTitle = headers[i].getAttribute('title');
|
||||||
|
const headerText = headers[i].textContent?.trim();
|
||||||
|
log(`Nagłówek ${i}: title="${headerTitle}", text="${headerText}"`);
|
||||||
|
|
||||||
|
if (headerTitle === title) {
|
||||||
|
log(`Znaleziono kolumnę "${title}" na pozycji ${i}`);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Nie znaleziono kolumny "${title}"`);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Główna funkcja dodawania kopiowania ———
|
||||||
|
function addCopyFunctionality(table) {
|
||||||
|
log('Dodawanie funkcjonalności kopiowania do tabeli');
|
||||||
|
log('Selektor tabeli:', table.className, table.tagName);
|
||||||
|
|
||||||
|
// Iteruj przez wszystkie wiersze danych
|
||||||
|
const rows = table.querySelectorAll('tbody tr.o_data_row');
|
||||||
|
log(`Znaleziono ${rows.length} wierszy danych`);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
// Spróbuj alternatywnych selektorów
|
||||||
|
const altRows = table.querySelectorAll('tbody tr');
|
||||||
|
log(`Alternatywnie znaleziono ${altRows.length} wierszy (wszystkie)`);
|
||||||
|
|
||||||
|
altRows.forEach((row, rowIndex) => {
|
||||||
|
log(`Wiersz ${rowIndex}:`, row.className, row.children.length, 'komórek');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach((row, rowIndex) => {
|
||||||
|
log(`Przetwarzanie wiersza ${rowIndex} z ${row.children.length} komórkami`);
|
||||||
|
|
||||||
|
// Dla każdej kolumny do kopiowania
|
||||||
|
Object.entries(COPYABLE_COLUMNS).forEach(([title, dataName]) => {
|
||||||
|
const colIndex = findColumnByTitle(table, title);
|
||||||
|
if (colIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = row.children[colIndex];
|
||||||
|
if (!cell) {
|
||||||
|
log(`Brak komórki w wierszu ${rowIndex}, kolumna ${colIndex}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Dodawanie kopiowania do komórki [${rowIndex}, ${colIndex}] "${title}": "${cell.textContent?.trim()}"`);
|
||||||
|
|
||||||
|
// Specjalne traktowanie dla kolumny Produkt
|
||||||
|
if (title === 'Produkt') {
|
||||||
|
makeProductCellCopyable(cell);
|
||||||
|
} else {
|
||||||
|
makeCellCopyable(cell);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Dodawanie kopiowania dla pól formularza ———
|
||||||
|
function addFormFieldsCopyFunctionality() {
|
||||||
|
log('Dodawanie kopiowania dla pól formularza...');
|
||||||
|
|
||||||
|
Object.entries(COPYABLE_FORM_FIELDS).forEach(([fieldName, displayName]) => {
|
||||||
|
// Szukaj różnych selektorów dla pól
|
||||||
|
let selectors = [
|
||||||
|
`span[name="${fieldName}"]`,
|
||||||
|
`a[name="${fieldName}"]`,
|
||||||
|
`input[name="${fieldName}"]`,
|
||||||
|
`[name="${fieldName}"] span`,
|
||||||
|
`[name="${fieldName}"]`
|
||||||
|
];
|
||||||
|
|
||||||
|
// Specjalne selektory dla konkretnych pól
|
||||||
|
if (fieldName === 'invoice_name') {
|
||||||
|
selectors = [
|
||||||
|
'span[name="invoice_name"]',
|
||||||
|
'input[name="invoice_name"]',
|
||||||
|
'[name="invoice_name"]'
|
||||||
|
];
|
||||||
|
} else if (fieldName === 'name') {
|
||||||
|
selectors = [
|
||||||
|
'span[name="name"]',
|
||||||
|
'input[name="name"]',
|
||||||
|
'[name="name"]',
|
||||||
|
'h1 span[name="name"]'
|
||||||
|
];
|
||||||
|
} else if (fieldName === 'partner_vat') {
|
||||||
|
selectors = [
|
||||||
|
'span[name="partner_vat"]',
|
||||||
|
'input[name="partner_vat"]',
|
||||||
|
'[name="partner_vat"]',
|
||||||
|
'span[name="vat"]',
|
||||||
|
'input[name="vat"]',
|
||||||
|
'[name="vat"]'
|
||||||
|
];
|
||||||
|
} else if (fieldName === 'note' || fieldName === 'narration') {
|
||||||
|
selectors = [
|
||||||
|
`span[name="${fieldName}"]`,
|
||||||
|
`textarea[name="${fieldName}"]`,
|
||||||
|
`div[name="${fieldName}"]`,
|
||||||
|
`[name="${fieldName}"]`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
for (const selector of selectors) {
|
||||||
|
try {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element && element.textContent?.trim()) {
|
||||||
|
log(`Znaleziono pole ${displayName} (${fieldName}):`, element.tagName, element.textContent?.trim());
|
||||||
|
makeFormFieldCopyable(element, displayName);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Błąd selektora "${selector}":`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jeśli nie znaleziono standardowymi selektorami, spróbuj szukać po etykietach
|
||||||
|
if (!found) {
|
||||||
|
if (fieldName === 'partner_vat') {
|
||||||
|
const labels = document.querySelectorAll('label');
|
||||||
|
for (const label of labels) {
|
||||||
|
if (label.textContent?.includes('NIP') || label.textContent?.includes('VAT')) {
|
||||||
|
const nextElement = label.nextElementSibling?.querySelector('span') ||
|
||||||
|
label.parentElement?.nextElementSibling?.querySelector('span');
|
||||||
|
if (nextElement && nextElement.textContent?.trim()) {
|
||||||
|
log(`Znaleziono ${displayName} przez etykietę:`, nextElement.textContent?.trim());
|
||||||
|
makeFormFieldCopyable(nextElement, displayName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (fieldName === 'note' || fieldName === 'narration') {
|
||||||
|
const labels = document.querySelectorAll('label');
|
||||||
|
for (const label of labels) {
|
||||||
|
if (label.textContent?.includes('Uwagi') || label.textContent?.includes('Notatki') || label.textContent?.includes('Opis')) {
|
||||||
|
const nextElement = label.nextElementSibling?.querySelector('span, textarea, div') ||
|
||||||
|
label.parentElement?.nextElementSibling?.querySelector('span, textarea, div');
|
||||||
|
if (nextElement && nextElement.textContent?.trim()) {
|
||||||
|
log(`Znaleziono ${displayName} przez etykietę:`, nextElement.textContent?.trim());
|
||||||
|
makeFormFieldCopyable(nextElement, displayName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Dodawanie kopiowania dla pól podsumowania ———
|
||||||
|
function addSummaryFieldsCopyFunctionality() {
|
||||||
|
log('Dodawanie kopiowania dla pól podsumowania...');
|
||||||
|
|
||||||
|
Object.entries(COPYABLE_SUMMARY_FIELDS).forEach(([fieldName, displayName]) => {
|
||||||
|
// Szukaj różnych selektorów dla pól podsumowania
|
||||||
|
const selectors = [
|
||||||
|
`span[name="${fieldName}"]`,
|
||||||
|
`[name="${fieldName}"]`,
|
||||||
|
`.oe_subtotal_footer_separator[name="${fieldName}"]`
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of selectors) {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
log(`Znaleziono pole ${displayName} (${fieldName}):`, element.textContent?.trim());
|
||||||
|
makeSummaryFieldCopyable(element, displayName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dodatkowe szukanie VAT - znaczek kopiowania przy wartościach, nie nazwach
|
||||||
|
const vatRows = document.querySelectorAll('tr:has(.oe_tax_group_name)');
|
||||||
|
vatRows.forEach((row, index) => {
|
||||||
|
const nameElement = row.querySelector('.oe_tax_group_name');
|
||||||
|
const valueElement = row.querySelector('.oe_tax_group_amount_value, .oe_tax_group_editable span');
|
||||||
|
|
||||||
|
if (nameElement && valueElement && nameElement.textContent?.includes('VAT')) {
|
||||||
|
const vatName = nameElement.textContent?.trim() || `VAT ${index + 1}`;
|
||||||
|
log(`Znaleziono VAT "${vatName}":`, valueElement.textContent?.trim());
|
||||||
|
makeSummaryFieldCopyable(valueElement, vatName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback - jeśli powyższe nie zadziała, szukaj bezpośrednio wartości VAT
|
||||||
|
const vatValues = document.querySelectorAll('.oe_tax_group_amount_value');
|
||||||
|
vatValues.forEach((element, index) => {
|
||||||
|
if (!element.dataset.tmSummaryCopyable) {
|
||||||
|
log(`Znaleziono wartość VAT ${index + 1}:`, element.textContent?.trim());
|
||||||
|
makeSummaryFieldCopyable(element, `VAT ${index + 1}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Lokalizowanie docelowego miejsca dla przycisku ———
|
||||||
|
function findActionsContainer() {
|
||||||
|
log('Szukanie kontenera dla przycisku...');
|
||||||
|
|
||||||
|
// PRIORYTET 1: Dokładnie jak w idea-orders.user.js - szukaj notebook_page_130
|
||||||
|
const byId = document.getElementById('notebook_page_130');
|
||||||
|
if (byId) {
|
||||||
|
log('Znaleziono notebook_page_130');
|
||||||
|
return byId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORYTET 2: Szukaj przycisku load_from_pricelist i jego kontener .tab-pane
|
||||||
|
const pricelistBtn = document.querySelector('button[name="load_from_pricelist"]');
|
||||||
|
if (pricelistBtn) {
|
||||||
|
const container = pricelistBtn.closest('.tab-pane') || pricelistBtn.parentElement;
|
||||||
|
log('Znaleziono kontener przez load_from_pricelist:', container.id || container.className);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORYTET 3: Szukaj w .tab-content > .tab-pane (gdzie są zakładki)
|
||||||
|
const tabPanes = document.querySelectorAll('.tab-content .tab-pane');
|
||||||
|
for (const pane of tabPanes) {
|
||||||
|
// Szukaj zakładki z przyciskami (load_from_pricelist lub button[name="523"])
|
||||||
|
if (pane.querySelector('button[name="load_from_pricelist"], button[name="523"]')) {
|
||||||
|
log('Znaleziono zakładkę z przyciskami:', pane.id || pane.className);
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORYTET 4: Szukaj konkretnych zakładek KFZ - notebook_page_669 (Pozycje faktury)
|
||||||
|
const kfzTab = document.getElementById('notebook_page_669');
|
||||||
|
if (kfzTab) {
|
||||||
|
log('Znaleziono notebook_page_669 (Pozycje faktury)');
|
||||||
|
return kfzTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Nie znaleziono odpowiedniego kontenera');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Przycisk przełączania trybu kopiowania produktów ———
|
||||||
|
function addCopyModeToggleButton() {
|
||||||
|
const host = findActionsContainer();
|
||||||
|
if (!host) {
|
||||||
|
log('Nie znaleziono kontenera dla przycisku');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idBtn = 'tm-kfz-copy-mode-toggle-button';
|
||||||
|
if (host.querySelector('#' + idBtn)) {
|
||||||
|
log('Przycisk już istnieje');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Tworzenie przycisku przełączania trybu kopiowania...');
|
||||||
|
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.id = idBtn;
|
||||||
|
btn.className = 'btn btn-secondary'; // Dokładnie jak w idea-orders
|
||||||
|
const label = document.createElement('span');
|
||||||
|
btn.appendChild(label);
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true);
|
||||||
|
label.textContent = copyOnlySKU ? 'Kopiuję: SKU' : 'Kopiuję: Pełna nazwa';
|
||||||
|
btn.title = copyOnlySKU ?
|
||||||
|
'Kliknij aby przełączyć na kopiowanie pełnej nazwy produktu' :
|
||||||
|
'Kliknij aby przełączyć na kopiowanie tylko SKU produktu';
|
||||||
|
log('Odświeżono przycisk, tryb:', copyOnlySKU ? 'SKU' : 'Pełna nazwa');
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const newMode = !GM_getValue(COPY_MODE_KEY, true);
|
||||||
|
GM_setValue(COPY_MODE_KEY, newMode);
|
||||||
|
refresh();
|
||||||
|
log('Zmieniono tryb kopiowania na:', newMode ? 'SKU' : 'Pełna nazwa');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dokładnie ta sama logika umieszczania jak w idea-orders.user.js
|
||||||
|
const massBtn = host.querySelector('button[name="523"]');
|
||||||
|
if (massBtn && massBtn.parentElement) {
|
||||||
|
massBtn.insertAdjacentElement('afterend', btn);
|
||||||
|
log('Dodano przycisk obok button[name="523"]');
|
||||||
|
} else {
|
||||||
|
host.appendChild(btn);
|
||||||
|
log('Dodano przycisk do kontenera:', host.className || host.tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Menu Tampermonkey ———
|
||||||
|
GM_registerMenuCommand('Ustawienia KFZ ▶', () => {
|
||||||
|
const choice = prompt(
|
||||||
|
'Ustawienia KFZ (wpisz numer):\n' +
|
||||||
|
'1) Przełącz tryb kopiowania (SKU ⇄ Pełna nazwa)\n' +
|
||||||
|
'2) Test kopiowania produktów\n' +
|
||||||
|
'3) Wyświetl aktualny tryb kopiowania'
|
||||||
|
);
|
||||||
|
if (!choice) return;
|
||||||
|
|
||||||
|
switch (choice.trim()) {
|
||||||
|
case '1':
|
||||||
|
const currentMode = GM_getValue(COPY_MODE_KEY, true);
|
||||||
|
GM_setValue(COPY_MODE_KEY, !currentMode);
|
||||||
|
const newModeText = !currentMode ? 'SKU' : 'Pełna nazwa';
|
||||||
|
alert(`Tryb kopiowania zmieniony na: ${newModeText}`);
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
const tables = document.querySelectorAll(TABLE_SEL);
|
||||||
|
if (tables.length > 0) {
|
||||||
|
addCopyFunctionality(tables[0]);
|
||||||
|
alert('Funkcja kopiowania została zastosowana do tabeli KFZ.');
|
||||||
|
} else {
|
||||||
|
alert('Nie znaleziono tabeli KFZ na tej stronie.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
const mode = GM_getValue(COPY_MODE_KEY, true);
|
||||||
|
alert(`Aktualny tryb kopiowania: ${mode ? 'SKU' : 'Pełna nazwa'}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// nic
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ——— Główna procedura ———
|
||||||
|
function processOnce() {
|
||||||
|
log('=== ROZPOCZĘCIE processOnce ===');
|
||||||
|
|
||||||
|
if (!isKFZForm()) {
|
||||||
|
log('Nie jest to formularz KFZ - kończenie');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Wykryto formularz KFZ, inicjalizacja...');
|
||||||
|
|
||||||
|
const tables = document.querySelectorAll(TABLE_SEL);
|
||||||
|
log(`Znaleziono ${tables.length} tabel z selektorem: ${TABLE_SEL}`);
|
||||||
|
|
||||||
|
if (!tables.length) {
|
||||||
|
// Spróbuj alternatywnych selektorów
|
||||||
|
const altTables = document.querySelectorAll('table.o_list_table');
|
||||||
|
log(`Alternatywnie znaleziono ${altTables.length} tabel z selektorem: table.o_list_table`);
|
||||||
|
|
||||||
|
const allTables = document.querySelectorAll('table');
|
||||||
|
log(`Wszystkich tabel na stronie: ${allTables.length}`);
|
||||||
|
|
||||||
|
allTables.forEach((table, i) => {
|
||||||
|
log(`Tabela ${i}:`, table.className);
|
||||||
|
});
|
||||||
|
|
||||||
|
addCopyModeToggleButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tables.forEach((table, i) => {
|
||||||
|
log(`Przetwarzanie tabeli ${i}:`, table.className);
|
||||||
|
addCopyFunctionality(table);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dodaj kopiowanie dla pól formularza i podsumowania
|
||||||
|
addFormFieldsCopyFunctionality();
|
||||||
|
addSummaryFieldsCopyFunctionality();
|
||||||
|
|
||||||
|
addCopyModeToggleButton();
|
||||||
|
log('=== ZAKOŃCZENIE processOnce ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Start + reagowanie na zmiany ———
|
||||||
|
let debounceTimer = null;
|
||||||
|
const debounce = (fn, ms = 150) => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(fn, ms);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inicjalizacja po załadowaniu DOM
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => debounce(processOnce, 150));
|
||||||
|
} else {
|
||||||
|
debounce(processOnce, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reagowanie na zmiany w DOM i hash
|
||||||
|
window.addEventListener('hashchange', () => debounce(processOnce, 250));
|
||||||
|
new MutationObserver(() => debounce(processOnce, 150))
|
||||||
|
.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
log('Skrypt KFZ załadowany');
|
||||||
|
})();
|
||||||
220
idea/idea-order-shipping.user.js
Normal file
220
idea/idea-order-shipping.user.js
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name IDEAERP - Listy przewozowe
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.0
|
||||||
|
// @description Kopiowanie elementów na stronach zamówień wysyłkowych - kliknij aby skopiować
|
||||||
|
// @match https://emma.ideaerp.pl/web*
|
||||||
|
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/order-shipping
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/order-shipping
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_deleteValue
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ========================OPIS==============================
|
||||||
|
WERSJA 1.0 - Kopiowanie elementów zamówień wysyłkowych
|
||||||
|
|
||||||
|
DZIAŁANIE SKRYPTU:
|
||||||
|
- Kopiowanie nazwy zamówienia - kliknij na nazwę zamówienia (h1) aby skopiować
|
||||||
|
- Kopiowanie numeru listu przewozowego - kliknij na element span[name="waybill"] aby skopiować
|
||||||
|
- Kopiowanie NIP faktury - kliknij na element span[name="invoice_vat"] aby skopiować
|
||||||
|
- Kopiowanie notatek - kliknij na element span[name="note"] aby skopiować
|
||||||
|
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
|
||||||
|
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
|
||||||
|
|
||||||
|
AUTOMATYCZNE AKTUALIZACJE:
|
||||||
|
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
|
||||||
|
- Jednoklikowa aktualizacja bez konieczności ręcznego pobierania
|
||||||
|
- Zachowanie ustawień użytkownika podczas aktualizacji
|
||||||
|
- Kompatybilność z systemem n8n do zarządzania wersjami
|
||||||
|
|
||||||
|
CHANGELOG:
|
||||||
|
v1.0 (2024) - PIERWSZA WERSJA SKRYPTU WYSYŁKOWEGO
|
||||||
|
- Utworzono dedykowany skrypt dla zamówień wysyłkowych (shipping.order)
|
||||||
|
- Dodano kopiowanie nazwy zamówienia (h1) z ikoną ⧉
|
||||||
|
- Dodano kopiowanie numeru listu przewozowego (span[name="waybill"]) z ikoną ⧉
|
||||||
|
- Dodano kopiowanie NIP faktury (span[name="invoice_vat"]) z ikoną ⧉
|
||||||
|
- Dodano kopiowanie notatek (span[name="note"]) z ikoną ⧉
|
||||||
|
- Dodano style CSS dla elementów span z ikonami kopiowania
|
||||||
|
- Dodano funkcję makeSpanCopyable() dla uniwersalnego kopiowania span
|
||||||
|
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
|
||||||
|
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
|
||||||
|
- Podwyższenie numeru wersji do 1.0
|
||||||
|
|
||||||
|
AUTOR: Adam Grodecki
|
||||||
|
DATA: 2024
|
||||||
|
LICENCJA: Własność EMMA SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ——— Stałe ———
|
||||||
|
const STATE_KEY = 'tm_shipping_price_unit_visible';
|
||||||
|
const INIT_FLAG = 'tm_shipping_init_flag';
|
||||||
|
const COPY_MODE_KEY = 'tm_shipping_copy_mode';
|
||||||
|
const WIDTHS_KEY = 'tm_shipping_widths';
|
||||||
|
|
||||||
|
// ——— Style CSS ———
|
||||||
|
GM_addStyle(`
|
||||||
|
/* Kopiowanie nazwy zamówienia – ikona dla h1 */
|
||||||
|
h1.tm-copyable { cursor: copy; }
|
||||||
|
h1.tm-copyable::after {
|
||||||
|
content: '⧉'; /* symbol kopiowania */
|
||||||
|
display: inline-block; /* w tej samej linii co tekst */
|
||||||
|
margin-left: 8px; /* odstęp od tekstu */
|
||||||
|
font-size: 14px; /* większy rozmiar dla h1 */
|
||||||
|
line-height: 1;
|
||||||
|
color: #ff9800; /* pomarańczowy */
|
||||||
|
opacity: .75; /* widoczna także bez hovera */
|
||||||
|
vertical-align: baseline;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .15s ease;
|
||||||
|
}
|
||||||
|
h1.tm-copyable:hover::after { opacity: 1; }
|
||||||
|
h1.tm-copyable.tm-copied {
|
||||||
|
outline: 2px solid #ff9800;
|
||||||
|
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kopiowanie dla elementów span z określonymi name */
|
||||||
|
span.tm-copyable { cursor: copy; }
|
||||||
|
span.tm-copyable::after {
|
||||||
|
content: '⧉'; /* symbol kopiowania */
|
||||||
|
display: inline-block; /* w tej samej linii co tekst */
|
||||||
|
margin-left: 6px; /* odstęp od tekstu */
|
||||||
|
font-size: 12px; /* rozmiar jak dla komórek tabeli */
|
||||||
|
line-height: 1;
|
||||||
|
color: #ff9800; /* pomarańczowy */
|
||||||
|
opacity: .75; /* widoczna także bez hovera */
|
||||||
|
vertical-align: baseline;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .15s ease;
|
||||||
|
}
|
||||||
|
span.tm-copyable:hover::after { opacity: 1; }
|
||||||
|
span.tm-copyable.tm-copied {
|
||||||
|
outline: 2px solid #ff9800;
|
||||||
|
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// ——— Pomocnicze ———
|
||||||
|
function isShippingOrderForm() {
|
||||||
|
const params = new URLSearchParams((location.hash || '').replace(/^#/, ''));
|
||||||
|
const model = params.get('model');
|
||||||
|
const viewType = params.get('view_type');
|
||||||
|
return model === 'shipping.order' && viewType === 'form';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tmCopyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Kopiowanie nazwy zamówienia ———
|
||||||
|
function makeOrderNameCopyable(h1Element) {
|
||||||
|
if (!h1Element || h1Element.dataset.tmOrderNameCopyable) return;
|
||||||
|
h1Element.dataset.tmOrderNameCopyable = '1';
|
||||||
|
h1Element.classList.add('tm-copyable');
|
||||||
|
h1Element.style.cursor = 'pointer';
|
||||||
|
h1Element.title = 'Kliknij aby skopiować nazwę zamówienia';
|
||||||
|
|
||||||
|
h1Element.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const orderName = (h1Element.textContent || '').trim();
|
||||||
|
if (!orderName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tmCopyToClipboard(orderName);
|
||||||
|
const old = h1Element.title;
|
||||||
|
h1Element.title = `Skopiowano nazwę zamówienia: ${orderName}`;
|
||||||
|
h1Element.classList.add('tm-copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
h1Element.title = old || 'Kliknij aby skopiować nazwę zamówienia';
|
||||||
|
h1Element.classList.remove('tm-copied');
|
||||||
|
}, 1200);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Tampermonkey] Błąd podczas kopiowania nazwy zamówienia:', error);
|
||||||
|
h1Element.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true); // capture=true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Kopiowanie elementów span z określonymi name ———
|
||||||
|
function makeSpanCopyable(spanElement, fieldName) {
|
||||||
|
if (!spanElement || spanElement.dataset.tmSpanCopyable) return;
|
||||||
|
spanElement.dataset.tmSpanCopyable = '1';
|
||||||
|
spanElement.classList.add('tm-copyable');
|
||||||
|
spanElement.style.cursor = 'pointer';
|
||||||
|
spanElement.title = `Kliknij aby skopiować ${fieldName}`;
|
||||||
|
|
||||||
|
spanElement.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const text = (spanElement.textContent || '').trim();
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tmCopyToClipboard(text);
|
||||||
|
const old = spanElement.title;
|
||||||
|
spanElement.title = `Skopiowano ${fieldName}: ${text}`;
|
||||||
|
spanElement.classList.add('tm-copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
spanElement.title = old || `Kliknij aby skopiować ${fieldName}`;
|
||||||
|
spanElement.classList.remove('tm-copied');
|
||||||
|
}, 1200);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Tampermonkey] Błąd podczas kopiowania ${fieldName}:`, error);
|
||||||
|
spanElement.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true); // capture=true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Główna procedura ———
|
||||||
|
function processOnce() {
|
||||||
|
if (!isShippingOrderForm()) return;
|
||||||
|
if (!GM_getValue(INIT_FLAG)) { GM_setValue(INIT_FLAG, true); }
|
||||||
|
|
||||||
|
// Dodaj kopiowanie nazwy zamówienia
|
||||||
|
const orderNameSpan = document.querySelector('h1 span[name="name"]');
|
||||||
|
if (orderNameSpan) {
|
||||||
|
const orderNameH1 = orderNameSpan.closest('h1');
|
||||||
|
if (orderNameH1) {
|
||||||
|
makeOrderNameCopyable(orderNameH1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dodaj kopiowanie dla elementów span z określonymi name
|
||||||
|
const waybillSpan = document.querySelector('span[name="waybill"]');
|
||||||
|
if (waybillSpan) {
|
||||||
|
makeSpanCopyable(waybillSpan, 'numer listu przewozowego');
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceVatSpan = document.querySelector('span[name="invoice_vat"]');
|
||||||
|
if (invoiceVatSpan) {
|
||||||
|
makeSpanCopyable(invoiceVatSpan, 'NIP faktury');
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteSpan = document.querySelector('span[name="note"]');
|
||||||
|
if (noteSpan) {
|
||||||
|
makeSpanCopyable(noteSpan, 'notatkę');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Start + reagowanie na zmiany ———
|
||||||
|
let deb = null; const debounce = (fn, ms = 150) => { clearTimeout(deb); deb = setTimeout(fn, ms); };
|
||||||
|
window.addEventListener('load', () => debounce(processOnce, 150));
|
||||||
|
window.addEventListener('hashchange', () => debounce(processOnce, 250));
|
||||||
|
new MutationObserver(() => debounce(processOnce, 150)).observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
758
idea/idea-orders.user.js
Executable file
758
idea/idea-orders.user.js
Executable file
|
|
@ -0,0 +1,758 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name IDEAERP - Zamówienia
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 3.7
|
||||||
|
// @description Dodano kopiowanie nazwy zamówienia, NIP faktury i notatek - kliknij aby skopiować
|
||||||
|
// @match https://emma.ideaerp.pl/web*
|
||||||
|
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/order
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/order
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_deleteValue
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ========================OPIS==============================
|
||||||
|
WERSJA 3.7 - Rozszerzone kopiowanie elementów
|
||||||
|
|
||||||
|
WYMAGANE KOLUMNY w tabeli IdeaERP:
|
||||||
|
- product_uom_qty → Ilość
|
||||||
|
- order_line_netto → Netto (lub price_subtotal jako zamiennik)
|
||||||
|
- order_line_gross → Brutto
|
||||||
|
- price_unit → Cena jednostkowa
|
||||||
|
- product_id → Produkt
|
||||||
|
|
||||||
|
DZIAŁANIE SKRYPTU:
|
||||||
|
- Wszystkie funkcje z v3.6 PLUS:
|
||||||
|
- Kopiowanie nazwy zamówienia - kliknij na nazwę zamówienia (h1) aby skopiować
|
||||||
|
- Kopiowanie NIP faktury - kliknij na element span[name="invoice_vat"] aby skopiować
|
||||||
|
- Kopiowanie notatek - kliknij na element span[name="note"] aby skopiować
|
||||||
|
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
|
||||||
|
- Naprawione wyświetlanie kolumny "Cena jednostkowa" po włączeniu
|
||||||
|
- Lepsze zarządzanie szerokościami tabeli
|
||||||
|
- Optymalizacja layoutu dla różnych rozdzielczości
|
||||||
|
|
||||||
|
POPRAWKI WZGLĘDEM v3.1:
|
||||||
|
+ Naprawione zarządzanie szerokością tabeli (table-layout: auto !important)
|
||||||
|
+ Zmniejszone domyślne szerokości kolumn (90px zamiast 120px)
|
||||||
|
+ Ulepszona funkcja setPriceUnitVisibility() z lepszym zarządzaniem stylów
|
||||||
|
+ Konkretne wymiary dla kolumny "Cena jednostkowa" (120px-180px)
|
||||||
|
+ Wymuszenie elastycznego layoutu przy pokazywaniu kolumny
|
||||||
|
+ Czyszczenie stylów komórek przy przełączaniu widoczności
|
||||||
|
+ Dodatkowe zabezpieczenia w processOnce() dla table-layout
|
||||||
|
|
||||||
|
ROZWIĄZANE PROBLEMY:
|
||||||
|
- Kolumna "Cena jednostkowa" nie była widoczna po kliknięciu "Pokaż"
|
||||||
|
- Konflikty między table-layout: fixed a dynamicznymi kolumnami
|
||||||
|
- Problemy z szerokościami na różnych rozdzielczościach
|
||||||
|
- Brak miejsca w tabeli po dodaniu nowych kolumn
|
||||||
|
|
||||||
|
ZAPAMIĘTYWANIE USTAWIEŃ:
|
||||||
|
- Wszystkie ustawienia (widoczność kolumn, tryb kopiowania, szerokości)
|
||||||
|
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 });
|
||||||
|
})();
|
||||||
308
idea/idea-payment-register.user.js
Normal file
308
idea/idea-payment-register.user.js
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name IDEAERP - Rejestracja Płatności
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.0
|
||||||
|
// @description Dodano kopiowanie kwoty w oknie rejestracji płatności
|
||||||
|
// @match https://emma.ideaerp.pl/web*
|
||||||
|
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/idea-payment-register
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/idea-payment-register
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_deleteValue
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ========================OPIS==============================
|
||||||
|
WERSJA 1.0 - Kopiowanie kwoty płatności
|
||||||
|
|
||||||
|
DZIAŁANIE SKRYPTU:
|
||||||
|
- Wykrywa okno modalne "Rejestruj płatność"
|
||||||
|
- Dodaje ikonę kopiowania (⧉) przy polu "Kwota" (amount)
|
||||||
|
- Kliknięcie kopiuje wartość do schowka
|
||||||
|
- Obsługa błędów i logowanie
|
||||||
|
|
||||||
|
CHANGELOG:
|
||||||
|
v1.0 (2024) - PIERWSZA WERSJA
|
||||||
|
- Dodano obsługę okna modalnego
|
||||||
|
- Dodano kopiowanie pola amount
|
||||||
|
- Stylizacja zgodna z innymi skryptami
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ——— Style CSS ———
|
||||||
|
GM_addStyle(`
|
||||||
|
/* Kopiowanie – ikona tuż za liczbą (inline), mniejsza, pomarańczowa */
|
||||||
|
.tm-copyable-icon {
|
||||||
|
cursor: copy;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #ff9800;
|
||||||
|
opacity: .75;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: opacity .15s ease;
|
||||||
|
}
|
||||||
|
.tm-copyable-icon:hover { opacity: 1; }
|
||||||
|
.tm-copied {
|
||||||
|
color: #4caf50 !important; /* Zielony po skopiowaniu */
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// ——— Schowek ———
|
||||||
|
async function tmCopyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Tampermonkey] Clipboard API failed, trying fallback', err);
|
||||||
|
try {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Tampermonkey] Fallback copy failed', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Główna logika ———
|
||||||
|
function processModal(modal) {
|
||||||
|
if (modal.dataset.tmProcessed) return;
|
||||||
|
|
||||||
|
// Sprawdź czy to modal płatności
|
||||||
|
const title = modal.querySelector('.modal-title');
|
||||||
|
const isPaymentModal = title && (title.textContent.includes('Rejestruj płatność') || title.textContent.includes('Register Payment'));
|
||||||
|
|
||||||
|
if (!isPaymentModal) {
|
||||||
|
// Jeśli nie ma tytułu, sprawdzamy czy są pola specyficzne dla płatności
|
||||||
|
if (!modal.querySelector('input[name="amount"]')) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Tampermonkey] Przetwarzanie modalu płatności...');
|
||||||
|
|
||||||
|
// Lista pól do obsłużenia
|
||||||
|
const fieldsToProcess = [
|
||||||
|
{ name: 'amount', label: 'Kwota' },
|
||||||
|
{ name: 'amount_div', label: 'Kwota' }, // Fallback dla struktury z amount_div
|
||||||
|
{ name: 'payment_date', label: 'Data płatności' },
|
||||||
|
{ name: 'communication', label: 'Notatka' },
|
||||||
|
{ name: 'ref', label: 'Referencja' }
|
||||||
|
];
|
||||||
|
|
||||||
|
fieldsToProcess.forEach(field => {
|
||||||
|
// Priorytet dla inputa - szukamy najpierw inputa
|
||||||
|
let element = modal.querySelector(`input[name="${field.name}"]`);
|
||||||
|
|
||||||
|
// Jeśli nie ma inputa, szukamy div lub span
|
||||||
|
if (!element) {
|
||||||
|
element = modal.querySelector(`div[name="${field.name}"], span[name="${field.name}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
console.log(`[Tampermonkey] Znaleziono pole ${field.name}:`, element);
|
||||||
|
addCopyIcon(element, field.label);
|
||||||
|
|
||||||
|
// Dla pola kwoty dodaj dodatkowy wiersz z VAT
|
||||||
|
if (field.name === 'amount' || field.name === 'amount_div') {
|
||||||
|
addVatRow(element, modal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.dataset.tmProcessed = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVatRow(amountElement, modal) {
|
||||||
|
// Znajdź wiersz tabeli (tr) zawierający pole kwoty
|
||||||
|
const amountRow = amountElement.closest('tr');
|
||||||
|
if (!amountRow) return;
|
||||||
|
|
||||||
|
// Sprawdź czy wiersz VAT już istnieje
|
||||||
|
if (amountRow.nextElementSibling && amountRow.nextElementSibling.classList.contains('tm-vat-row')) return;
|
||||||
|
|
||||||
|
// Ustal element z wartością (input) - priorytet dla inputa wewnątrz kontenera
|
||||||
|
let valueElement = amountElement;
|
||||||
|
if (amountElement.tagName !== 'INPUT' && amountElement.querySelector('input')) {
|
||||||
|
valueElement = amountElement.querySelector('input');
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAmountValue = () => {
|
||||||
|
let valStr = '';
|
||||||
|
if (valueElement.tagName === 'INPUT') {
|
||||||
|
valStr = valueElement.value;
|
||||||
|
} else {
|
||||||
|
valStr = valueElement.textContent;
|
||||||
|
}
|
||||||
|
// Używamy tej samej logiki czyszczenia co przy kopiowaniu
|
||||||
|
valStr = valStr.replace(/[^\d,.-]/g, '').replace(',', '.');
|
||||||
|
return parseFloat(valStr) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Oblicz VAT (Brutto -> VAT)
|
||||||
|
// Wzór: VAT = Brutto * 23 / 123
|
||||||
|
const calculateVat = (val) => {
|
||||||
|
if (!val) return '0,00';
|
||||||
|
const vat = (val * 23) / 123;
|
||||||
|
return vat.toFixed(2).replace('.', ',');
|
||||||
|
};
|
||||||
|
|
||||||
|
const amountVal = getAmountValue();
|
||||||
|
const calculatedValue = calculateVat(amountVal);
|
||||||
|
|
||||||
|
// Stwórz nowy wiersz
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = 'tm-vat-row';
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td class="o_td_label">
|
||||||
|
<label class="o_form_label">Kwota VAT (obl.)</label>
|
||||||
|
</td>
|
||||||
|
<td style="width: 100%; display: flex; align-items: center;">
|
||||||
|
<span class="tm-vat-value" style="margin-right: 5px; font-weight: bold;">${calculatedValue}</span>
|
||||||
|
<span class="tm-copyable-icon" title="Kliknij aby skopiować Kwota VAT">⧉</span>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Wstaw za wierszem kwoty
|
||||||
|
amountRow.parentNode.insertBefore(tr, amountRow.nextSibling);
|
||||||
|
|
||||||
|
// Obsługa kopiowania dla nowego pola
|
||||||
|
const copyIcon = tr.querySelector('.tm-copyable-icon');
|
||||||
|
const valueSpan = tr.querySelector('.tm-vat-value');
|
||||||
|
|
||||||
|
copyIcon.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const val = valueSpan.textContent;
|
||||||
|
if (val) {
|
||||||
|
const success = await tmCopyToClipboard(val);
|
||||||
|
if (success) {
|
||||||
|
copyIcon.classList.add('tm-copied');
|
||||||
|
setTimeout(() => copyIcon.classList.remove('tm-copied'), 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nasłuchiwanie zmian w polu kwoty (jeśli to input)
|
||||||
|
if (valueElement.tagName === 'INPUT') {
|
||||||
|
valueElement.addEventListener('input', () => {
|
||||||
|
const newVal = getAmountValue();
|
||||||
|
valueSpan.textContent = calculateVat(newVal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCopyIcon(element, label) {
|
||||||
|
// Ustal element docelowy dla ikony i kontener
|
||||||
|
let targetContainer = element.parentElement;
|
||||||
|
let valueElement = element;
|
||||||
|
let isDatepicker = false;
|
||||||
|
|
||||||
|
// Jeśli element to div/span (np. datepicker wrapper), ale zawiera inputa, to chcemy inputa
|
||||||
|
if (element.tagName !== 'INPUT' && element.querySelector('input')) {
|
||||||
|
valueElement = element.querySelector('input');
|
||||||
|
// W przypadku datepickera (div), on sam jest kontenerem
|
||||||
|
targetContainer = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprawdź czy to datepicker
|
||||||
|
if (targetContainer.classList.contains('o_datepicker')) {
|
||||||
|
isDatepicker = true;
|
||||||
|
// Dla datepickera wychodzimy poziom wyżej (do td), żeby nie psuć wewnętrznego layoutu (ikona kalendarza)
|
||||||
|
if (targetContainer.parentElement) {
|
||||||
|
targetContainer = targetContainer.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprawdź czy ikona już nie istnieje w kontenerze
|
||||||
|
if (targetContainer && targetContainer.querySelector('.tm-copyable-icon')) return;
|
||||||
|
|
||||||
|
const icon = document.createElement('span');
|
||||||
|
icon.className = 'tm-copyable-icon';
|
||||||
|
icon.textContent = '⧉';
|
||||||
|
icon.title = `Kliknij aby skopiować ${label}`;
|
||||||
|
|
||||||
|
icon.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
let valueToCopy = '';
|
||||||
|
if (valueElement.tagName === 'INPUT') {
|
||||||
|
valueToCopy = valueElement.value;
|
||||||
|
} else {
|
||||||
|
valueToCopy = valueElement.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Czyszczenie wartości
|
||||||
|
if (label === 'Kwota') {
|
||||||
|
valueToCopy = valueToCopy.replace(/[^\d,.-]/g, '').trim();
|
||||||
|
} else {
|
||||||
|
valueToCopy = valueToCopy.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueToCopy) {
|
||||||
|
const success = await tmCopyToClipboard(valueToCopy);
|
||||||
|
if (success) {
|
||||||
|
icon.classList.add('tm-copied');
|
||||||
|
icon.title = `Skopiowano: ${valueToCopy}`;
|
||||||
|
setTimeout(() => {
|
||||||
|
icon.classList.remove('tm-copied');
|
||||||
|
icon.title = `Kliknij aby skopiować ${label}`;
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wstawianie ikony i stylowanie
|
||||||
|
if (targetContainer) {
|
||||||
|
// Wymuś flex na kontenerze, żeby ikona była obok
|
||||||
|
targetContainer.style.display = 'flex';
|
||||||
|
targetContainer.style.alignItems = 'center';
|
||||||
|
|
||||||
|
// Jeśli to datepicker, element datepickera (dziecko) powinien zająć resztę miejsca
|
||||||
|
if (isDatepicker) {
|
||||||
|
const datepickerDiv = targetContainer.querySelector('.o_datepicker');
|
||||||
|
if (datepickerDiv) {
|
||||||
|
datepickerDiv.style.flex = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetContainer.appendChild(icon);
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
element.appendChild(icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Obserwator zmian DOM ———
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node.nodeType === 1) {
|
||||||
|
// Sprawdź czy dodano modal
|
||||||
|
if (node.classList.contains('modal') || node.classList.contains('o_technical_modal') || node.querySelector('.modal-dialog')) {
|
||||||
|
// Poczekaj chwilę na renderowanie zawartości
|
||||||
|
setTimeout(() => processModal(node), 500);
|
||||||
|
}
|
||||||
|
// Czasami modal jest już w DOM, ale zmienia się jego zawartość
|
||||||
|
const modal = node.closest('.modal');
|
||||||
|
if (modal) {
|
||||||
|
setTimeout(() => processModal(modal), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
})();
|
||||||
301
idea/idea-purchase.user.js
Normal file
301
idea/idea-purchase.user.js
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name IDEAERP - Zamówienia Zakupu
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.0
|
||||||
|
// @description Kopiowanie elementów na stronach zamówień zakupu - kliknij aby skopiować
|
||||||
|
// @match https://emma.ideaerp.pl/web*
|
||||||
|
// @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/
|
||||||
|
// @downloadURL https://n8n.emma.net.pl/webhook/purchase
|
||||||
|
// @updateURL https://n8n.emma.net.pl/webhook/purchase
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_deleteValue
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ========================OPIS==============================
|
||||||
|
WERSJA 1.0 - Kopiowanie elementów zamówień zakupu
|
||||||
|
|
||||||
|
DZIAŁANIE SKRYPTU:
|
||||||
|
- Kopiowanie nazwy zamówienia - kliknij na nazwę zamówienia (h1) aby skopiować
|
||||||
|
- Kopiowanie produktów z kolumny "Produkt" - kliknij na produkt aby skopiować SKU lub pełną nazwę
|
||||||
|
- Tryb kopiowania SKU/pełna nazwa - przełączalny przycisk
|
||||||
|
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
|
||||||
|
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
|
||||||
|
|
||||||
|
AUTOMATYCZNE AKTUALIZACJE:
|
||||||
|
- Tampermonkey automatycznie sprawdza dostępność nowych wersji
|
||||||
|
- Jednoklikowa aktualizacja bez konieczności ręcznego pobierania
|
||||||
|
- Zachowanie ustawień użytkownika podczas aktualizacji
|
||||||
|
- Kompatybilność z systemem n8n do zarządzania wersjami
|
||||||
|
|
||||||
|
CHANGELOG:
|
||||||
|
v1.0 (2024) - PIERWSZA WERSJA SKRYPTU ZAKUPOWEGO
|
||||||
|
- Utworzono dedykowany skrypt dla zamówień zakupu (purchase.order)
|
||||||
|
- Dodano kopiowanie nazwy zamówienia (h1) z ikoną ⧉
|
||||||
|
- Dodano kopiowanie produktów z kolumny "Produkt" z ikoną ⧉
|
||||||
|
- Dodano tryb kopiowania SKU/pełna nazwa z przyciskiem przełączania
|
||||||
|
- Dodano funkcję extractSKU() do wyodrębniania SKU z nawiasów kwadratowych
|
||||||
|
- Dodano style CSS dla elementów h1 i td z ikonami kopiowania
|
||||||
|
- Dodano funkcje makeOrderNameCopyable() i makeProductCellCopyable()
|
||||||
|
- Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules
|
||||||
|
- Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800)
|
||||||
|
- Podwyższenie numeru wersji do 1.0
|
||||||
|
|
||||||
|
AUTOR: Adam Grodecki
|
||||||
|
DATA: 2024
|
||||||
|
LICENCJA: Własność EMMA SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ——— Stałe ———
|
||||||
|
const COPY_MODE_KEY = 'tm_purchase_copy_mode';
|
||||||
|
const INIT_FLAG = 'tm_purchase_init_flag';
|
||||||
|
|
||||||
|
// ——— Style CSS ———
|
||||||
|
GM_addStyle(`
|
||||||
|
/* Kopiowanie nazwy zamówienia – ikona dla h1 */
|
||||||
|
h1.tm-copyable { cursor: copy; }
|
||||||
|
h1.tm-copyable::after {
|
||||||
|
content: '⧉'; /* symbol kopiowania */
|
||||||
|
display: inline-block; /* w tej samej linii co tekst */
|
||||||
|
margin-left: 8px; /* odstęp od tekstu */
|
||||||
|
font-size: 14px; /* większy rozmiar dla h1 */
|
||||||
|
line-height: 1;
|
||||||
|
color: #ff9800; /* pomarańczowy */
|
||||||
|
opacity: .75; /* widoczna także bez hovera */
|
||||||
|
vertical-align: baseline;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .15s ease;
|
||||||
|
}
|
||||||
|
h1.tm-copyable:hover::after { opacity: 1; }
|
||||||
|
h1.tm-copyable.tm-copied {
|
||||||
|
outline: 2px solid #ff9800;
|
||||||
|
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kopiowanie produktów – ikona tuż za tekstem (inline), mniejsza, pomarańczowa */
|
||||||
|
td.tm-copyable { cursor: copy; }
|
||||||
|
td.tm-copyable::after {
|
||||||
|
content: '⧉'; /* symbol kopiowania w kolorze CSS */
|
||||||
|
display: inline-block; /* w tej samej linii co tekst */
|
||||||
|
margin-left: 6px; /* mały odstęp od tekstu */
|
||||||
|
font-size: 12px; /* mniejszy rozmiar */
|
||||||
|
line-height: 1;
|
||||||
|
color: #ff9800; /* pomarańczowy */
|
||||||
|
opacity: .75; /* widoczna także bez hovera */
|
||||||
|
vertical-align: baseline;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .15s ease;
|
||||||
|
}
|
||||||
|
td.tm-copyable:hover::after { opacity: 1; }
|
||||||
|
td.tm-copyable.tm-copied {
|
||||||
|
outline: 2px solid #ff9800;
|
||||||
|
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Przycisk przełączania trybu kopiowania w zakładkach */
|
||||||
|
.tm-copy-mode-btn {
|
||||||
|
background: #ff9800 !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
margin-left: 10px !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
.tm-copy-mode-btn:hover {
|
||||||
|
background: #f57c00 !important;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// ——— Pomocnicze ———
|
||||||
|
function isPurchaseOrderForm() {
|
||||||
|
const params = new URLSearchParams((location.hash || '').replace(/^#/, ''));
|
||||||
|
const model = params.get('model');
|
||||||
|
const viewType = params.get('view_type');
|
||||||
|
return model === 'purchase.order' && viewType === 'form';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tmCopyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Funkcja wyodrębniania SKU z nawiasów kwadratowych ———
|
||||||
|
function extractSKU(productText) {
|
||||||
|
const match = productText.match(/\[([^\]]+)\]/);
|
||||||
|
return match ? match[1] : productText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Kopiowanie nazwy zamówienia ———
|
||||||
|
function makeOrderNameCopyable(h1Element) {
|
||||||
|
if (!h1Element || h1Element.dataset.tmOrderNameCopyable) return;
|
||||||
|
h1Element.dataset.tmOrderNameCopyable = '1';
|
||||||
|
h1Element.classList.add('tm-copyable');
|
||||||
|
h1Element.style.cursor = 'pointer';
|
||||||
|
h1Element.title = 'Kliknij aby skopiować nazwę zamówienia';
|
||||||
|
|
||||||
|
h1Element.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const orderName = (h1Element.textContent || '').trim();
|
||||||
|
if (!orderName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tmCopyToClipboard(orderName);
|
||||||
|
const old = h1Element.title;
|
||||||
|
h1Element.title = `Skopiowano nazwę zamówienia: ${orderName}`;
|
||||||
|
h1Element.classList.add('tm-copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
h1Element.title = old || 'Kliknij aby skopiować nazwę zamówienia';
|
||||||
|
h1Element.classList.remove('tm-copied');
|
||||||
|
}, 1200);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Tampermonkey] Błąd podczas kopiowania nazwy zamówienia:', error);
|
||||||
|
h1Element.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true); // capture=true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Kopiowanie produktów z trybem SKU/pełna nazwa ———
|
||||||
|
function makeProductCellCopyable(td) {
|
||||||
|
if (!td || td.dataset.tmProductCopyable) return;
|
||||||
|
td.dataset.tmProductCopyable = '1';
|
||||||
|
td.classList.add('tm-copyable');
|
||||||
|
td.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
|
||||||
|
const fullText = (td.textContent || '').trim();
|
||||||
|
if (!fullText) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true); // domyślnie SKU
|
||||||
|
const textToCopy = copyOnlySKU ? extractSKU(fullText) : fullText;
|
||||||
|
|
||||||
|
await tmCopyToClipboard(textToCopy);
|
||||||
|
const old = td.title;
|
||||||
|
const mode = copyOnlySKU ? 'SKU' : 'pełną nazwę';
|
||||||
|
td.title = `Skopiowano ${mode}: ${textToCopy}`;
|
||||||
|
td.classList.add('tm-copied');
|
||||||
|
setTimeout(() => { td.title = old || 'Skopiowano'; td.classList.remove('tm-copied'); }, 1200);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Tampermonkey] Błąd podczas kopiowania produktu:', error);
|
||||||
|
td.title = 'Błąd podczas kopiowania';
|
||||||
|
}
|
||||||
|
}, true); // capture=true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Lokalizowanie docelowego miejsca dla przycisku ———
|
||||||
|
function findActionsContainer() {
|
||||||
|
// Znajdź kontener przycisków poniżej zakładek
|
||||||
|
const massBtn = document.querySelector('button[name="523"]');
|
||||||
|
if (massBtn) return massBtn.closest('.tab-pane') || massBtn.parentElement;
|
||||||
|
|
||||||
|
// Fallback - znajdź kontener o_form_buttons
|
||||||
|
const buttonsContainer = document.querySelector('.o_form_buttons');
|
||||||
|
if (buttonsContainer) return buttonsContainer;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Dodaj przycisk przełączania trybu kopiowania ———
|
||||||
|
function addCopyModeToggleButton() {
|
||||||
|
const host = findActionsContainer();
|
||||||
|
if (!host) {
|
||||||
|
console.log('[Tampermonkey] Nie znaleziono kontenera akcji, pomijam dodawanie przycisku');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idBtn = 'tm-purchase-copy-mode-toggle-button';
|
||||||
|
if (host.querySelector('#' + idBtn)) {
|
||||||
|
console.log('[Tampermonkey] Przycisk już istnieje, pomijam');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Tampermonkey] Znaleziono kontener akcji:', host);
|
||||||
|
console.log('[Tampermonkey] Tworzę przycisk przełączania trybu kopiowania');
|
||||||
|
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.id = idBtn;
|
||||||
|
btn.className = 'btn btn-secondary';
|
||||||
|
const label = document.createElement('span');
|
||||||
|
btn.appendChild(label);
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true);
|
||||||
|
label.textContent = copyOnlySKU ? 'Kopiuję: SKU' : 'Kopiuję: Pełna nazwa';
|
||||||
|
btn.title = copyOnlySKU ? 'Kliknij aby przełączyć na kopiowanie pełnej nazwy produktu' : 'Kliknij aby przełączyć na kopiowanie tylko SKU produktu';
|
||||||
|
console.log('[Tampermonkey] Tryb kopiowania:', copyOnlySKU ? 'SKU' : 'Pełna nazwa');
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const currentMode = GM_getValue(COPY_MODE_KEY, true);
|
||||||
|
GM_setValue(COPY_MODE_KEY, !currentMode);
|
||||||
|
refresh();
|
||||||
|
console.log('[Tampermonkey] Tryb kopiowania zmieniony na:', !currentMode ? 'SKU' : 'Pełna nazwa');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dodaj przycisk do kontenera przycisków (zawsze poniżej zakładek)
|
||||||
|
const massBtn = host.querySelector('button[name="523"]');
|
||||||
|
if (massBtn && massBtn.parentElement) {
|
||||||
|
massBtn.insertAdjacentElement('afterend', btn);
|
||||||
|
console.log('[Tampermonkey] Przycisk dodany obok przycisku Masowa zmiana');
|
||||||
|
} else {
|
||||||
|
host.appendChild(btn);
|
||||||
|
console.log('[Tampermonkey] Przycisk dodany do kontenera akcji');
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Główna procedura ———
|
||||||
|
function processOnce() {
|
||||||
|
if (!isPurchaseOrderForm()) return;
|
||||||
|
if (!GM_getValue(INIT_FLAG)) { GM_setValue(INIT_FLAG, true); }
|
||||||
|
|
||||||
|
// Dodaj kopiowanie nazwy zamówienia
|
||||||
|
const orderNameSpan = document.querySelector('h1 span[name="name"]');
|
||||||
|
if (orderNameSpan) {
|
||||||
|
const orderNameH1 = orderNameSpan.closest('h1');
|
||||||
|
if (orderNameH1) {
|
||||||
|
makeOrderNameCopyable(orderNameH1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dodaj kopiowanie produktów z kolumny "Produkt"
|
||||||
|
// Komórki produktów mają klasę o_list_many2one i są w 3. kolumnie tabeli
|
||||||
|
const productCells = document.querySelectorAll('td.o_data_cell.o_field_cell.o_list_many2one.o_readonly_modifier.o_required_modifier');
|
||||||
|
productCells.forEach(cell => {
|
||||||
|
// Sprawdź czy to rzeczywiście komórka produktu (zawiera nawiasy kwadratowe z SKU)
|
||||||
|
const text = cell.textContent || '';
|
||||||
|
if (text.includes('[') && text.includes(']')) {
|
||||||
|
makeProductCellCopyable(cell);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dodaj przycisk przełączania trybu kopiowania
|
||||||
|
addCopyModeToggleButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Start + reagowanie na zmiany ———
|
||||||
|
let deb = null; const debounce = (fn, ms = 150) => { clearTimeout(deb); deb = setTimeout(fn, ms); };
|
||||||
|
window.addEventListener('load', () => debounce(processOnce, 150));
|
||||||
|
window.addEventListener('hashchange', () => debounce(processOnce, 250));
|
||||||
|
new MutationObserver(() => debounce(processOnce, 150)).observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
1023
idea/kfz-listing.html
Normal file
1023
idea/kfz-listing.html
Normal file
File diff suppressed because it is too large
Load diff
2254
idea/kfz.html
Normal file
2254
idea/kfz.html
Normal file
File diff suppressed because one or more lines are too long
2725
idea/orders.html
Normal file
2725
idea/orders.html
Normal file
File diff suppressed because one or more lines are too long
0
idea/purchase.html
Normal file
0
idea/purchase.html
Normal file
Loading…
Reference in a new issue