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>
122 lines
5.9 KiB
JavaScript
122 lines
5.9 KiB
JavaScript
/**
|
||
* 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}`); }
|
||
});
|
||
};
|