/** * Skrift – Bestands-Hook (Auto-Abbuchung) * --------------------------------------------------------------------------- * Bucht Material automatisch ab, sobald ein Druck-Job als „printed" gemeldet * wird (Abgang BEIM DRUCK), am Produktionsstandort des Kunden (leer = Skrift): * * Brief gedruckt → Papier (physisches Format) × Menge × 1,10 * Kuvert gedruckt → Kuvert (envelope_format) × Menge × 1,10 * + Briefmarke, NUR wenn von uns frankiert: * Inland → Standard, Ausland → International * (bzw. porto_klasse-Override) × Menge × 1,10 * * „Von uns frankiert" = order.frankiert, sonst automatisch: Einzelversand ja, * Sammelversand nein. +10 % Puffer (Zusatzversorgung), aufgerundet. * * Idempotent über jobs.inventory_done. Fehlende Bestandszeilen werden * übersprungen (geloggt), damit nie ein Druck blockiert. */ // Auftrags-Format-Key (formats.key) → physischer Papier-Key (inventory.format). const PAPIER = { a4: 'a4', a5p: 'a5', a5l: 'a5', a5_hoch: 'a5', a5_quer: 'a5', a6p: 'a6', a6l: 'a6', a6_hoch: 'a6', a6_quer: 'a6', dinlang_karte: 'dinlang', }; // +10 % Zusatzversorgung, aufgerundet. const puffer = (n) => Math.ceil((Number(n) || 0) * 1.1); // Anzahl aus einem numbers-String (z. B. „1-50", „1,3,5-7"). Leer = 0. function anzahlAusNumbers(s) { const txt = String(s || '').trim(); if (!txt) return 0; let n = 0; for (const teil of txt.split(',')) { const t = teil.trim(); const m = t.match(/^(\d+)\s*-\s*(\d+)$/); if (m) n += Math.max(0, Number(m[2]) - Number(m[1]) + 1); else if (/^\d+$/.test(t)) n += 1; } return n; } export default (register, { services, getSchema, logger }) => { const { ItemsService } = services; let skriftId = null; // Cache: Standort-ID „Skrift" (Default). async function defaultStandort(svc) { if (skriftId) return skriftId; const rows = await svc('standorte').readByQuery({ filter: { name: { _eq: 'Skrift' } }, limit: 1, fields: ['id'] }); skriftId = (rows && rows[0]) ? rows[0].id : null; return skriftId; } async function abbuchen(svc, standortId, kategorie, formatKey, menge, orderId) { if (!standortId || menge <= 0 || !formatKey) return; const rows = await svc('inventory').readByQuery({ filter: { _and: [{ standort: { _eq: standortId } }, { kategorie: { _eq: kategorie } }, { format: { _eq: String(formatKey) } }] }, limit: 1, fields: ['id', 'bestand', 'mindestbestand', 'bezeichnung'], }); const row = rows && rows[0]; if (!row) { logger.info(`[skrift-inventory] keine Bestandszeile ${kategorie}/${formatKey} @Standort ${standortId} – übersprungen`); return; } const neu = (Number(row.bestand) || 0) - menge; await svc('inventory').updateOne(row.id, { bestand: neu }); await svc('inventory_log').createOne({ inventory: row.id, delta: -menge, grund: 'druck', order: orderId || null }); if (neu < (Number(row.mindestbestand) || 0)) { logger.warn(`[skrift-inventory] Mindestbestand unterschritten: ${row.bezeichnung || formatKey} (${neu})`); } } async function verbucheJob(svc, jobId) { const job = await svc('jobs').readOne(jobId, { fields: ['id', 'type', 'status', 'numbers', 'inventory_done', 'order.id', 'order.format.key', 'order.envelope_format', 'order.entries_count', 'order.count_inland', 'order.count_ausland', 'order.shipping_type', 'order.frankiert', 'order.porto_klasse', 'order.customer.produktionsstandort'], }); if (!job || job.status !== 'printed' || job.inventory_done || !job.order) return; const o = job.order; const standortId = o.customer?.produktionsstandort || await defaultStandort(svc); if (!standortId) { logger.info('[skrift-inventory] kein Standort (auch kein „Skrift") – übersprungen'); return; } const menge = anzahlAusNumbers(job.numbers) || Number(o.entries_count) || 0; if (menge <= 0) { await svc('jobs').updateOne(jobId, { inventory_done: true }); return; } if (job.type === 'umschlag') { await abbuchen(svc, standortId, 'kuvert', o.envelope_format || 'dinlang', puffer(menge), o.id); // Briefmarke nur, wenn von uns frankiert (Default: Einzel ja / Sammel nein). const frankiert = (typeof o.frankiert === 'boolean') ? o.frankiert : (o.shipping_type !== 'sammel'); if (frankiert) { const inland = Number(o.count_inland) || 0; const ausland = Number(o.count_ausland) || 0; const klasse = (o.porto_klasse && o.porto_klasse !== 'auto') ? o.porto_klasse : null; if (klasse) { await abbuchen(svc, standortId, 'briefmarke', klasse, puffer((inland || ausland) ? inland + ausland : menge), o.id); } else { const nInland = (inland || ausland) ? inland : menge; // keine Länderzählung → alles Inland if (nInland > 0) await abbuchen(svc, standortId, 'briefmarke', 'standard', puffer(nInland), o.id); if (ausland > 0) await abbuchen(svc, standortId, 'briefmarke', 'international', puffer(ausland), o.id); } } } else { // Brief → Papier (physisches Format). 1 Blatt je Brief (mehrseitig: späteres TODO). const papierKey = PAPIER[String(o.format?.key || 'a4').toLowerCase()] || 'a4'; await abbuchen(svc, standortId, 'papier', papierKey, puffer(menge), o.id); } await svc('jobs').updateOne(jobId, { inventory_done: true }); } register.action('jobs.items.update', async ({ keys, payload }) => { if (!payload || payload.status !== 'printed') return; try { const schema = await getSchema(); const svc = (c) => new ItemsService(c, { schema, accountability: null }); for (const key of (keys || [])) { try { await verbucheJob(svc, key); } catch (e) { logger.warn(`[skrift-inventory] Job ${key}: ${e.message}`); } } } catch (e) { logger.warn(`[skrift-inventory] update-Hook: ${e.message}`); } }); };