Ich wollte mal ein kleines ChatGPT-Script mit euch teilen, das ich mir „herausgepromptet“ habe 🙂 War mir blöd, monatlich einer Firma, die diese Dateien automatisiert runterladen könnte, schlappe 30 Euro im Monat zu bezahlen...
Ich teile hier nur die Idee und wie es bei mir funktioniert hat.
Hier der Aliexpress Script:
// ==UserScript==
// @name AliExpress: Rechnungen aller sichtbaren Bestellungen laden
// @namespace ae-invoice-bulk
// @version 1.0
// @description Lädt die Rechnungen (PDF) für alle sichtbaren Bestellungen mit passendem Dateinamen herunter.
// @author you
// @match https://www.aliexpress.com/p/order/index.html*
// @match https://www.aliexpress.com/p/order/index.htm*
// @ICON https://www.aliexpress.com/favicon.ico
// @run-at document-idle
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect trade.aliexpress.com
// ==/UserScript==
(function () {
'use strict';
/*** UI-Button einfügen ***/
function addButton() {
const btn = document.createElement('button');
btn.textContent = 'Rechnungen (sichtbar) laden';
btn.style.position = 'fixed';
btn.style.right = '16px';
btn.style.bottom = '16px';
btn.style.zIndex = '999999';
btn.style.padding = '10px 14px';
btn.style.borderRadius = '8px';
btn.style.border = 'none';
btn.style.background = '#1677ff';
btn.style.color = '#fff';
btn.style.fontWeight = '600';
btn.style.cursor = 'pointer';
btn.title = 'Lädt die Rechnungen für alle aktuell sichtbaren Bestellungen';
btn.addEventListener('click', run);
document.body.appendChild(btn);
}
/*** Hilfen ***/
const MONTHS_DE = {
jan: 1, januar: 1,
feb: 2, februar: 2,
mär: 3, maerz: 3, märz: 3, mar: 3, march: 3,
apr: 4, april: 4,
mai: 5,
jun: 6, juni: 6,
jul: 7, juli: 7,
aug: 8, august: 8,
sep: 9, sept: 9, september: 9,
okt: 10, oktober: 10,
nov: 11, november: 11,
dez: 12, dezember: 12
};
function pad2(n) { return String(n).padStart(2, '0'); }
function parseGermanDateToYYYYMMDD(txt) {
// erwartet z. B. "24. Aug 2025"
const m = txt.trim().match(/(\d{1,2})\.\s*([A-Za-zäöüÄÖÜ]+)\s*(\d{4})/);
if (!m) return null;
const d = parseInt(m[1], 10);
const monKey = m[2].toLowerCase()
.replace('ä', 'ä') // bleibt
.replace('ö', 'o')
.replace('ü', 'u')
.replace('ß', 'ss')
.replace('mär', 'mär'); // gängigste Abk.
const mon = MONTHS_DE[monKey] || MONTHS_DE[monKey.slice(0,3)];
const y = parseInt(m[3], 10);
if (!mon) return null;
return `${y}${pad2(mon)}${pad2(d)}`;
}
function extractOrderInfo(orderEl) {
// Bestellnummer
const orderIdMatch = orderEl.textContent.match(/Bestellnummer:\s*(\d{6,})/);
const orderId = orderIdMatch ? orderIdMatch[1] : null;
// Kaufdatum
const dateDiv = Array.from(orderEl.querySelectorAll('.order-item-header-right-info div'))
.find(d => /Bestellung aufgegeben am/i.test(d.textContent));
const dateText = dateDiv ? dateDiv.textContent.replace(/.*?:\s*/, '') : '';
const yyyymmdd = parseGermanDateToYYYYMMDD(dateText);
// Gesamtbetrag
let totalText = '';
const totalEl = orderEl.querySelector('[data-pl="order_item_content_price_total"]');
if (totalEl) {
// enthält „Gesamt: 21,39 €“
totalText = totalEl.innerText || totalEl.textContent || '';
} else {
// Fallback: suche irgendein „Gesamt:“ mit € in der Nähe
const hint = Array.from(orderEl.querySelectorAll('span,div'))
.find(n => /Gesamt/i.test(n.textContent) && /€/.test(n.textContent));
totalText = hint ? hint.textContent : '';
}
const amountMatch = totalText.replace(/\s+/g, ' ').match(/([\d.,]+)\s*€/);
let amountStr = amountMatch ? amountMatch[1] : '';
// Normalisieren -> Zahl und danach wieder nach DE-Format
if (amountStr) {
const val = parseFloat(amountStr.replace(/\./g, '').replace(',', '.'));
if (!isNaN(val)) amountStr = val.toFixed(2).replace('.', ',');
}
return { orderId, yyyymmdd, amountStr };
}
function fetchInvoiceId(orderId) {
const url = `https://trade.aliexpress.com/ajax/invoice/queryInvoiceIdAjax.htm?orderIds=${orderId}`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
// Antwort ist i. d. R. ["908704618996"] oder []
resolve(Array.isArray(data) && data[0] ? String(data[0]) : null);
} catch (e) {
reject(e);
}
},
onerror: reject,
ontimeout: reject
});
});
}
function downloadInvoicePdf(invoiceId, orderId, filename) {
const url = `https://trade.aliexpress.com/ajax/invoice/invoiceExportAjax.htm?invoiceId=${encodeURIComponent(invoiceId)}&orderId=${encodeURIComponent(orderId)}`;
// Wir holen die PDF als Blob und übergeben sie dann an GM_download, damit Dateiname sicher gesetzt wird.
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: (res) => {
try {
const blob = res.response;
const objectUrl = URL.createObjectURL(blob);
GM_download({
url: objectUrl,
name: filename,
saveAs: false,
ontimeout: () => console.warn('Download timeout:', filename),
onerror: (e) => console.warn('Download error:', filename, e),
onload: () => {
// Object URL freigeben, wenn der Download gestartet wurde
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
}
});
} catch (e) {
console.error('PDF-Verarbeitung fehlgeschlagen', e);
}
},
onerror: (e) => console.error('PDF-Request fehlgeschlagen', e),
ontimeout: (e) => console.error('PDF-Request Timeout', e)
});
}
async function run() {
const items = Array.from(document.querySelectorAll('.order-item'));
if (!items.length) {
alert('Keine sichtbaren Bestellungen gefunden.');
return;
}
let ok = 0, skipped = 0;
for (const el of items) {
const { orderId, yyyymmdd, amountStr } = extractOrderInfo(el);
if (!orderId) { console.warn('Order ohne Bestellnummer übersprungen.'); skipped++; continue; }
const datePart = yyyymmdd || '00000000';
const amountPart = amountStr || '0,00';
try {
const invoiceId = await fetchInvoiceId(orderId);
if (!invoiceId) {
console.warn(`Keine invoiceId für Order ${orderId} (evtl. keine Rechnung vorhanden).`);
skipped++;
continue;
}
const filename = `${datePart}_${orderId}__EUR_${amountPart}.pdf`;
console. log('Lade Rechnung:', filename, ' invoiceId:', invoiceId);
downloadInvoicePdf(invoiceId, orderId, filename);
ok++;
} catch (e) {
console.error('Fehler bei Order', orderId, e);
skipped++;
}
}
setTimeout(() => {
alert(`Fertig: ${ok} Rechnung(en) angestoßen. Übersprungen: ${skipped}.`);
}, 100);
}
/*** Start ***/
addButton();
})();
und hier der Amazon Script:
// ==UserScript==
// @name Amazon DE – Alle Rechnungs-PDFs laden
// @namespace ama-pdf-bulk
// @version 1.0
// @description Lädt auf /your-orders/orders alle Rechnungs-/Korrektur-PDFs je Bestellung herunter – inkl. mehrfach vorhandener PDFs – und benennt sie nach Wunschschema.
// @author you
// @match https://www.amazon.de/your-orders/orders*
// @grant GM_download
// @grant GM_xmlhttpRequest
// @connect www.amazon.de
// ==/UserScript==
(function () {
'use strict';
// ---------- kleine Hilfen ----------
const $ = (sel, el = document) => el.querySelector(sel);
const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel));
const MONTHS_DE = {
'januar': 1, 'februar': 2, 'märz': 3, 'maerz': 3, 'april': 4,
'mai': 5, 'juni': 6, 'juli': 7, 'august': 8, 'september': 9,
'oktober': 10, 'november': 11, 'dezember': 12
};
const sanitizeForFilename = (s) =>
s.trim()
.replace(/\s+/g, '-') // Leerzeichen → Bindestrich
.replace(/[\u2013\u2014]/g, '-') // Gedankenstriche normalisieren
.replace(/[^A-Za-z0-9ÄÖÜäöüß\-]/g, ''); // ungültige Zeichen raus, Umlaute ok
function parseGermanOrderDateToYYYYMMDD(s) {
// Beispiele: "24. August 2025", "7. August 2025"
if (!s) return '00000000';
const m = s.trim()
.toLowerCase()
.replace(/\./g, '')
.replace('märz', 'maerz')
.match(/^(\d{1,2})\s+([a-zäöü]+)\s+(\d{4})$/i);
if (!m) return '00000000';
const dd = String(m[1]).padStart(2, '0');
const mon = MONTHS_DE[m[2]] || 0;
const mm = String(mon).padStart(2, '0');
const yyyy = m[3];
return `${yyyy}${mm}${dd}`;
}
function extractTextAfterLabel(container, labelText) {
// Sucht im "order-header" das Listenelement, dessen Mini-Label gleich labelText ist,
// und gibt den Wert im nachfolgenden .a-size-base zurück.
const items = $$('.order-header__header-list-item', container);
for (const li of items) {
const label = $('.a-size-mini .a-color-secondary', li);
const value = $('.a-size-base', li);
if (label && value && label.textContent.trim().toLowerCase() === labelText.toLowerCase()) {
return value.textContent.trim();
}
}
return '';
}
function absolute(url) {
try { return new URL(url, location.origin).href; } catch { return url; }
}
function gmGet(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
headers: { 'Accept': 'text/html,application/xhtml+xml' },
// Cookies mitnehmen (wichtig, weil die Popover-URL auth-gebunden ist)
anonymous: false,
// manche TM-Versionen nutzen withCredentials:
// @ts-ignore
withCredentials: true,
onload: (res) => resolve(res.responseText),
onerror: (e) => reject(e)
});
});
}
async function downloadAllForOrder(orderCard) {
try {
// Header-Infos ermitteln
const header = $('.order-header', orderCard) || orderCard;
const orderDateHuman = extractTextAfterLabel(header, 'Bestellung aufgegeben');
const orderDate = parseGermanOrderDateToYYYYMMDD(orderDateHuman);
const sumRaw = extractTextAfterLabel(header, 'Summe')
.replace(/[€\s\u00A0]/g, '') // Euro-Symbol & NBSP entfernen
.replace(/EUR/i, '')
.trim();
const sumForFile = sumRaw.replace(/[^\d,]/g, ''); // "58,42" behalten
const orderId = $('.yohtmlc-order-id span[dir="ltr"]', header)?.textContent.trim() || 'UNKNOWN';
// Link zum Popover (enthält alle Rechnungs-/Korrektur-Links als HTML)
const invoiceTrigger = $$('a.a-link-normal', header)
.find(a => /\/invoice\/invoice\.html/i.test(a.getAttribute('href') || '') ||
/\/shared-cs\/ajax\/invoice\/invoice\.html/i.test(a.getAttribute('href') || '') ||
a.textContent.trim().toLowerCase().startsWith('rechnung'));
if (!invoiceTrigger) {
console.warn('Keine Rechnungslinks für Bestellung gefunden:', orderId);
return;
}
// Popover-HTML abrufen
const popoverUrl = absolute(invoiceTrigger.getAttribute('href'));
const html = await gmGet(popoverUrl);
// HTML parsen und alle PDF-Links herausfischen
const dp = new DOMParser();
const doc = dp.parseFromString(html, 'text/html');
// robuste Suche: alle <a>, deren href .pdf enthält
const links = $$('a', doc).filter(a => {
const href = a.getAttribute('href') || '';
return /\.pdf(\?|$)/i.test(href);
});
if (!links.length) {
console.warn('Kein PDF-Link im Popover gefunden:', orderId, popoverUrl);
return;
}
for (let idx = 0; idx < links.length; idx++) {
const a = links[idx];
const href = absolute(a.getAttribute('href'));
let linkText = a.textContent.trim() || `Rechnung-${idx + 1}`;
// Einheitlich: Leerzeichen -> -, Sonderzeichen raus
linkText = sanitizeForFilename(linkText);
const filename = `${orderDate}_${linkText}_${orderId}__EUR_${sumForFile}.PDF`;
// Download starten
await new Promise((resolve) => {
GM_download({
url: href,
name: filename,
saveAs: false,
ontimeout: resolve,
onerror: resolve,
ontimeout: resolve,
onprogress: () => {},
onload: resolve
});
});
// kleine Pause, um Rate Limits zu vermeiden
await new Promise(r => setTimeout(r, 400));
}
} catch (e) {
console.error('Fehler bei Bestellung:', e);
}
}
// ---------- Button in die Seite einfügen ----------
function addUi() {
if ($('#tm-load-all-invoices')) return;
const btn = document.createElement('button');
btn.id = 'tm-load-all-invoices';
btn.textContent = 'Alle Rechnungs-PDFs laden';
Object.assign(btn.style, {
position: 'fixed',
bottom: '16px',
left: '16px',
zIndex: 99999,
background: '#ffd814',
color: '#111',
border: '1px solid #888',
borderRadius: '8px',
padding: '10px 14px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 'bold',
boxShadow: '0 2px 6px rgba(0,0,0,.2)'
});
btn.addEventListener('click', async () => {
btn.disabled = true;
btn.textContent = 'Lade PDFs…';
const cards = $$('.order-card');
for (const card of cards) {
await downloadAllForOrder(card);
}
btn.textContent = 'Fertig ✓';
setTimeout(() => { btn.disabled = false; btn.textContent = 'Alle Rechnungs-PDFs laden'; }, 3000);
});
document.body.appendChild(btn);
}
// ---------- Start ----------
const obs = new MutationObserver(() => addUi());
obs.observe(document.documentElement, { childList: true, subtree: true });
addUi();
})();
Hintergrund
- Ich habe kein Amazon Business Konto und konnte deshalb die Kaufrechnungen nicht gebündelt herunterladen – nur einzeln.
- Bei AliExpress gibt es den Button „Rechnung“ seit April 2025 nicht mehr. Davor konnte man Rechnungen mit MwSt. herunterladen, danach nur noch Eigenbelege (Netto), die man dem Finanzamt vorlegen muss.
→ Mit den hier vorgestellten Scripts bekomme ich jetzt wieder alle Rechnungen, bzw. sogar eine Ordner-Summary als PDF.
Installation
- Browser: Firefox
- Einstellung: PDFs sollen heruntergeladen und nicht im Browser geöffnet werden
- Add-on: Tampermonkey installieren
- Dann: im Tampermonkey-Addon auf „Neues Userscript erstellen“ klicken und die Scripts jeweils einfügen.
Bedienung
- AliExpress:
Rechts unten erscheint ein Button „Rechnungen (sichtbar) laden“.
Alle sichtbaren Rechnungen werden heruntergeladen. Wenn ihr mehr wollt, einfach weiterscrollen, bis alle Bestellungen geladen sind. - Amazon:
Alle Rechnungen der jeweils sichtbaren Seite werden heruntergeladen.
Für weitere Seiten müsst ihr einfach weiterblättern.
Wichtig: Die URL muss https://www.amazon.de/your-orders/orders lauten – dann erscheint links unten der Button „Alle Rechnungs-PDF laden“.
Hinweis
Ich teile hier nur die Idee und wie es bei mir funktioniert hat.
- Gewährleistung oder Haftung übernehme ich nicht – jeder ist für seine Daten selbst verantwortlich.
- Die Scripts generieren nichts Eigenes, sie laden nur herunter, was die Plattformen ohnehin bereitstellen.
- Support kann ich leider nicht bieten – aber: wenn ihr die Scripts erweitert oder verbessert, teilt es gerne hier! Vielleicht baut ja jemand auch etwas für eBay 😉 oder alles andere in gewissen erleichterungen auf diversen Plattformen
Hier der Aliexpress Script:
// ==UserScript==
// @name AliExpress: Rechnungen aller sichtbaren Bestellungen laden
// @namespace ae-invoice-bulk
// @version 1.0
// @description Lädt die Rechnungen (PDF) für alle sichtbaren Bestellungen mit passendem Dateinamen herunter.
// @author you
// @match https://www.aliexpress.com/p/order/index.html*
// @match https://www.aliexpress.com/p/order/index.htm*
// @ICON https://www.aliexpress.com/favicon.ico
// @run-at document-idle
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect trade.aliexpress.com
// ==/UserScript==
(function () {
'use strict';
/*** UI-Button einfügen ***/
function addButton() {
const btn = document.createElement('button');
btn.textContent = 'Rechnungen (sichtbar) laden';
btn.style.position = 'fixed';
btn.style.right = '16px';
btn.style.bottom = '16px';
btn.style.zIndex = '999999';
btn.style.padding = '10px 14px';
btn.style.borderRadius = '8px';
btn.style.border = 'none';
btn.style.background = '#1677ff';
btn.style.color = '#fff';
btn.style.fontWeight = '600';
btn.style.cursor = 'pointer';
btn.title = 'Lädt die Rechnungen für alle aktuell sichtbaren Bestellungen';
btn.addEventListener('click', run);
document.body.appendChild(btn);
}
/*** Hilfen ***/
const MONTHS_DE = {
jan: 1, januar: 1,
feb: 2, februar: 2,
mär: 3, maerz: 3, märz: 3, mar: 3, march: 3,
apr: 4, april: 4,
mai: 5,
jun: 6, juni: 6,
jul: 7, juli: 7,
aug: 8, august: 8,
sep: 9, sept: 9, september: 9,
okt: 10, oktober: 10,
nov: 11, november: 11,
dez: 12, dezember: 12
};
function pad2(n) { return String(n).padStart(2, '0'); }
function parseGermanDateToYYYYMMDD(txt) {
// erwartet z. B. "24. Aug 2025"
const m = txt.trim().match(/(\d{1,2})\.\s*([A-Za-zäöüÄÖÜ]+)\s*(\d{4})/);
if (!m) return null;
const d = parseInt(m[1], 10);
const monKey = m[2].toLowerCase()
.replace('ä', 'ä') // bleibt
.replace('ö', 'o')
.replace('ü', 'u')
.replace('ß', 'ss')
.replace('mär', 'mär'); // gängigste Abk.
const mon = MONTHS_DE[monKey] || MONTHS_DE[monKey.slice(0,3)];
const y = parseInt(m[3], 10);
if (!mon) return null;
return `${y}${pad2(mon)}${pad2(d)}`;
}
function extractOrderInfo(orderEl) {
// Bestellnummer
const orderIdMatch = orderEl.textContent.match(/Bestellnummer:\s*(\d{6,})/);
const orderId = orderIdMatch ? orderIdMatch[1] : null;
// Kaufdatum
const dateDiv = Array.from(orderEl.querySelectorAll('.order-item-header-right-info div'))
.find(d => /Bestellung aufgegeben am/i.test(d.textContent));
const dateText = dateDiv ? dateDiv.textContent.replace(/.*?:\s*/, '') : '';
const yyyymmdd = parseGermanDateToYYYYMMDD(dateText);
// Gesamtbetrag
let totalText = '';
const totalEl = orderEl.querySelector('[data-pl="order_item_content_price_total"]');
if (totalEl) {
// enthält „Gesamt: 21,39 €“
totalText = totalEl.innerText || totalEl.textContent || '';
} else {
// Fallback: suche irgendein „Gesamt:“ mit € in der Nähe
const hint = Array.from(orderEl.querySelectorAll('span,div'))
.find(n => /Gesamt/i.test(n.textContent) && /€/.test(n.textContent));
totalText = hint ? hint.textContent : '';
}
const amountMatch = totalText.replace(/\s+/g, ' ').match(/([\d.,]+)\s*€/);
let amountStr = amountMatch ? amountMatch[1] : '';
// Normalisieren -> Zahl und danach wieder nach DE-Format
if (amountStr) {
const val = parseFloat(amountStr.replace(/\./g, '').replace(',', '.'));
if (!isNaN(val)) amountStr = val.toFixed(2).replace('.', ',');
}
return { orderId, yyyymmdd, amountStr };
}
function fetchInvoiceId(orderId) {
const url = `https://trade.aliexpress.com/ajax/invoice/queryInvoiceIdAjax.htm?orderIds=${orderId}`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
// Antwort ist i. d. R. ["908704618996"] oder []
resolve(Array.isArray(data) && data[0] ? String(data[0]) : null);
} catch (e) {
reject(e);
}
},
onerror: reject,
ontimeout: reject
});
});
}
function downloadInvoicePdf(invoiceId, orderId, filename) {
const url = `https://trade.aliexpress.com/ajax/invoice/invoiceExportAjax.htm?invoiceId=${encodeURIComponent(invoiceId)}&orderId=${encodeURIComponent(orderId)}`;
// Wir holen die PDF als Blob und übergeben sie dann an GM_download, damit Dateiname sicher gesetzt wird.
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: (res) => {
try {
const blob = res.response;
const objectUrl = URL.createObjectURL(blob);
GM_download({
url: objectUrl,
name: filename,
saveAs: false,
ontimeout: () => console.warn('Download timeout:', filename),
onerror: (e) => console.warn('Download error:', filename, e),
onload: () => {
// Object URL freigeben, wenn der Download gestartet wurde
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
}
});
} catch (e) {
console.error('PDF-Verarbeitung fehlgeschlagen', e);
}
},
onerror: (e) => console.error('PDF-Request fehlgeschlagen', e),
ontimeout: (e) => console.error('PDF-Request Timeout', e)
});
}
async function run() {
const items = Array.from(document.querySelectorAll('.order-item'));
if (!items.length) {
alert('Keine sichtbaren Bestellungen gefunden.');
return;
}
let ok = 0, skipped = 0;
for (const el of items) {
const { orderId, yyyymmdd, amountStr } = extractOrderInfo(el);
if (!orderId) { console.warn('Order ohne Bestellnummer übersprungen.'); skipped++; continue; }
const datePart = yyyymmdd || '00000000';
const amountPart = amountStr || '0,00';
try {
const invoiceId = await fetchInvoiceId(orderId);
if (!invoiceId) {
console.warn(`Keine invoiceId für Order ${orderId} (evtl. keine Rechnung vorhanden).`);
skipped++;
continue;
}
const filename = `${datePart}_${orderId}__EUR_${amountPart}.pdf`;
console. log('Lade Rechnung:', filename, ' invoiceId:', invoiceId);
downloadInvoicePdf(invoiceId, orderId, filename);
ok++;
} catch (e) {
console.error('Fehler bei Order', orderId, e);
skipped++;
}
}
setTimeout(() => {
alert(`Fertig: ${ok} Rechnung(en) angestoßen. Übersprungen: ${skipped}.`);
}, 100);
}
/*** Start ***/
addButton();
})();
und hier der Amazon Script:
// ==UserScript==
// @name Amazon DE – Alle Rechnungs-PDFs laden
// @namespace ama-pdf-bulk
// @version 1.0
// @description Lädt auf /your-orders/orders alle Rechnungs-/Korrektur-PDFs je Bestellung herunter – inkl. mehrfach vorhandener PDFs – und benennt sie nach Wunschschema.
// @author you
// @match https://www.amazon.de/your-orders/orders*
// @grant GM_download
// @grant GM_xmlhttpRequest
// @connect www.amazon.de
// ==/UserScript==
(function () {
'use strict';
// ---------- kleine Hilfen ----------
const $ = (sel, el = document) => el.querySelector(sel);
const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel));
const MONTHS_DE = {
'januar': 1, 'februar': 2, 'märz': 3, 'maerz': 3, 'april': 4,
'mai': 5, 'juni': 6, 'juli': 7, 'august': 8, 'september': 9,
'oktober': 10, 'november': 11, 'dezember': 12
};
const sanitizeForFilename = (s) =>
s.trim()
.replace(/\s+/g, '-') // Leerzeichen → Bindestrich
.replace(/[\u2013\u2014]/g, '-') // Gedankenstriche normalisieren
.replace(/[^A-Za-z0-9ÄÖÜäöüß\-]/g, ''); // ungültige Zeichen raus, Umlaute ok
function parseGermanOrderDateToYYYYMMDD(s) {
// Beispiele: "24. August 2025", "7. August 2025"
if (!s) return '00000000';
const m = s.trim()
.toLowerCase()
.replace(/\./g, '')
.replace('märz', 'maerz')
.match(/^(\d{1,2})\s+([a-zäöü]+)\s+(\d{4})$/i);
if (!m) return '00000000';
const dd = String(m[1]).padStart(2, '0');
const mon = MONTHS_DE[m[2]] || 0;
const mm = String(mon).padStart(2, '0');
const yyyy = m[3];
return `${yyyy}${mm}${dd}`;
}
function extractTextAfterLabel(container, labelText) {
// Sucht im "order-header" das Listenelement, dessen Mini-Label gleich labelText ist,
// und gibt den Wert im nachfolgenden .a-size-base zurück.
const items = $$('.order-header__header-list-item', container);
for (const li of items) {
const label = $('.a-size-mini .a-color-secondary', li);
const value = $('.a-size-base', li);
if (label && value && label.textContent.trim().toLowerCase() === labelText.toLowerCase()) {
return value.textContent.trim();
}
}
return '';
}
function absolute(url) {
try { return new URL(url, location.origin).href; } catch { return url; }
}
function gmGet(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
headers: { 'Accept': 'text/html,application/xhtml+xml' },
// Cookies mitnehmen (wichtig, weil die Popover-URL auth-gebunden ist)
anonymous: false,
// manche TM-Versionen nutzen withCredentials:
// @ts-ignore
withCredentials: true,
onload: (res) => resolve(res.responseText),
onerror: (e) => reject(e)
});
});
}
async function downloadAllForOrder(orderCard) {
try {
// Header-Infos ermitteln
const header = $('.order-header', orderCard) || orderCard;
const orderDateHuman = extractTextAfterLabel(header, 'Bestellung aufgegeben');
const orderDate = parseGermanOrderDateToYYYYMMDD(orderDateHuman);
const sumRaw = extractTextAfterLabel(header, 'Summe')
.replace(/[€\s\u00A0]/g, '') // Euro-Symbol & NBSP entfernen
.replace(/EUR/i, '')
.trim();
const sumForFile = sumRaw.replace(/[^\d,]/g, ''); // "58,42" behalten
const orderId = $('.yohtmlc-order-id span[dir="ltr"]', header)?.textContent.trim() || 'UNKNOWN';
// Link zum Popover (enthält alle Rechnungs-/Korrektur-Links als HTML)
const invoiceTrigger = $$('a.a-link-normal', header)
.find(a => /\/invoice\/invoice\.html/i.test(a.getAttribute('href') || '') ||
/\/shared-cs\/ajax\/invoice\/invoice\.html/i.test(a.getAttribute('href') || '') ||
a.textContent.trim().toLowerCase().startsWith('rechnung'));
if (!invoiceTrigger) {
console.warn('Keine Rechnungslinks für Bestellung gefunden:', orderId);
return;
}
// Popover-HTML abrufen
const popoverUrl = absolute(invoiceTrigger.getAttribute('href'));
const html = await gmGet(popoverUrl);
// HTML parsen und alle PDF-Links herausfischen
const dp = new DOMParser();
const doc = dp.parseFromString(html, 'text/html');
// robuste Suche: alle <a>, deren href .pdf enthält
const links = $$('a', doc).filter(a => {
const href = a.getAttribute('href') || '';
return /\.pdf(\?|$)/i.test(href);
});
if (!links.length) {
console.warn('Kein PDF-Link im Popover gefunden:', orderId, popoverUrl);
return;
}
for (let idx = 0; idx < links.length; idx++) {
const a = links[idx];
const href = absolute(a.getAttribute('href'));
let linkText = a.textContent.trim() || `Rechnung-${idx + 1}`;
// Einheitlich: Leerzeichen -> -, Sonderzeichen raus
linkText = sanitizeForFilename(linkText);
const filename = `${orderDate}_${linkText}_${orderId}__EUR_${sumForFile}.PDF`;
// Download starten
await new Promise((resolve) => {
GM_download({
url: href,
name: filename,
saveAs: false,
ontimeout: resolve,
onerror: resolve,
ontimeout: resolve,
onprogress: () => {},
onload: resolve
});
});
// kleine Pause, um Rate Limits zu vermeiden
await new Promise(r => setTimeout(r, 400));
}
} catch (e) {
console.error('Fehler bei Bestellung:', e);
}
}
// ---------- Button in die Seite einfügen ----------
function addUi() {
if ($('#tm-load-all-invoices')) return;
const btn = document.createElement('button');
btn.id = 'tm-load-all-invoices';
btn.textContent = 'Alle Rechnungs-PDFs laden';
Object.assign(btn.style, {
position: 'fixed',
bottom: '16px',
left: '16px',
zIndex: 99999,
background: '#ffd814',
color: '#111',
border: '1px solid #888',
borderRadius: '8px',
padding: '10px 14px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 'bold',
boxShadow: '0 2px 6px rgba(0,0,0,.2)'
});
btn.addEventListener('click', async () => {
btn.disabled = true;
btn.textContent = 'Lade PDFs…';
const cards = $$('.order-card');
for (const card of cards) {
await downloadAllForOrder(card);
}
btn.textContent = 'Fertig ✓';
setTimeout(() => { btn.disabled = false; btn.textContent = 'Alle Rechnungs-PDFs laden'; }, 3000);
});
document.body.appendChild(btn);
}
// ---------- Start ----------
const obs = new MutationObserver(() => addUi());
obs.observe(document.documentElement, { childList: true, subtree: true });
addUi();
})();