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