Files
skrift-directus/extensions/directus-extension-skrift-inventory/dist/index.js
s4luorth f8f219a3fa Bestandspflege: Material je Standort, Auto-Abbuchung beim Druck
Datenmodell (bootstrap):
- Collections standorte (seed Skrift/Signascript), inventory (Papier/Kuvert/
  Briefmarke je Standort, initiale Zeilen) und inventory_log.
- directus_users.produktionsstandort (m2o standorte; USER_FIELDS-Relation wird
  jetzt angelegt); orders.frankiert + porto_klasse; jobs.inventory_done.

Neue Hook-Extension skrift-inventory:
- Bucht beim Job-Status 'printed' automatisch ab (am Kunden-Standort, +10 %):
  Brief->Papier (phys. Format), Kuvert->Kuvert + Briefmarke (nur wenn frankiert;
  Inland Standard / Ausland International, bzw. porto_klasse). Idempotent.

produktion: neuer Tab 'Bestand' - je Standort, Unterschreitung rot
('nachbestellen'), manuelles Zubuchen + Mindestbestand/Bestand editierbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-01 08:34:27 +02:00

122 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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}`); }
});
};