import { delivery as defaultDeliveryApi, ledger as defaultLedgerApi, stock as defaultStockApi } from '../api'; import artikelAutomat, { getArticle } from '../automats/artikelAutomat'; import articleIndexAutomat from '../automats/articleIndexAutomat'; /** * Determines whether a delivery line represents an article that tracks stock. * * @param {object} line Delivery line * @param {object|null} [article] Optional pre-resolved article * @returns {boolean} */ export function isLineStockTracked(line, article = null) { if (!line) return false; if (line.track_stock !== undefined) { return line.track_stock !== false && line.track_stock !== 0 && line.track_stock !== '0'; } if (line.trackStock !== undefined) { return Boolean(line.trackStock); } if (line.article?.track_stock !== undefined) { return line.article.track_stock !== false && line.article.track_stock !== 0 && line.article.track_stock !== '0'; } const sku = line.sku ? String(line.sku).trim() : null; if (!sku) return false; const art = article || getArticle(sku) || articleIndexAutomat.state.items?.find((item) => String(item.sku) === sku); if (art?.track_stock !== undefined) { return art.track_stock !== false && art.track_stock !== 0 && art.track_stock !== '0'; } return false; } /** * @typedef {Object} DeliveryLineLedgerStatus * @property {number} id - Manifestarticle ID * @property {string} sku - Article SKU * @property {string} name - Article Name * @property {number} targetAmount - Expected delivery quantity * @property {number} bookedAmount - Quantity booked in ledger * @property {number} remainingAmount - Quantity remaining to be booked * @property {boolean} isBooked - Whether this position is fully booked * @property {boolean} trackStock - Whether this article tracks inventory * @property {boolean} isBookable - Whether this position can be booked into the warehouse * @property {Array} ledgerEntries - Ledger entries for this line */ /** * @typedef {Object} DeliveryLedgerSummary * @property {number} deliveryId * @property {Object} delivery - Full delivery object with lines * @property {'empty' | 'booked' | 'partial' | 'unbooked'} status * @property {number} totalPositions * @property {number} bookedPositions * @property {number} totalQuantity * @property {number} bookedQuantity * @property {boolean} hasTrackStock * @property {Array} lines */ /** * Fetches delivery lines and checks ledger entries for each line to compute the booking status. * * @param {number|string} deliveryId * @param {Object} [options] * @param {Object} [options.deliveryApi] * @param {Object} [options.ledgerApi] * @returns {Promise} */ export async function fetchDeliveryLedgerSummary(deliveryId, { deliveryApi = defaultDeliveryApi, ledgerApi = defaultLedgerApi } = {}) { const res = await deliveryApi.get(deliveryId); const delivery = res?.delivery ?? res; const rawLines = Array.isArray(delivery?.lines) ? delivery.lines : []; if (rawLines.length === 0) { return { deliveryId, delivery, status: 'empty', totalPositions: 0, bookedPositions: 0, totalQuantity: 0, bookedQuantity: 0, hasTrackStock: false, lines: [], }; } // Fetch ledger entries for each line in parallel const linesWithLedger = await Promise.all( rawLines.map(async (line) => { let ledgerEntries = []; let article = null; const promises = [ ledgerApi.listByManifestarticleId(line.id).catch((err) => { console.error(`[deliveryLedger] Failed to fetch ledger for line ${line.id}:`, err); return null; }), ]; if (line.sku) { const cached = getArticle(line.sku); if (cached) { article = cached; } else { promises.push( artikelAutomat.actions.loadArticle(line.sku).catch(() => null) ); } } const results = await Promise.all(promises); const ledgerRes = results[0]; if (results[1]) { article = results[1]; } ledgerEntries = Array.isArray(ledgerRes?.ledger) ? ledgerRes.ledger : []; const trackStock = isLineStockTracked(line, article); const deliveryEntries = ledgerEntries.filter((e) => { const t = String(e.transaction_type).toUpperCase(); return t === 'ACCEPTANCE' || t === 'RETURN' || t === 'DELIVERY'; }); const bookedAmount = deliveryEntries.reduce((sum, e) => sum + Number(e.amount || 0), 0); const targetAmount = Number(line.amount || 0); const remainingAmount = Math.max(0, targetAmount - bookedAmount); const isBooked = targetAmount > 0 ? bookedAmount >= targetAmount : bookedAmount > 0; return { ...line, targetAmount, bookedAmount, remainingAmount, isBooked, trackStock, isBookable: true, ledgerEntries: deliveryEntries, }; }) ); const totalPositions = linesWithLedger.length; const bookedPositions = linesWithLedger.filter((l) => l.isBooked).length; const totalQuantity = linesWithLedger.reduce((sum, l) => sum + l.targetAmount, 0); const bookedQuantity = linesWithLedger.reduce((sum, l) => sum + l.bookedAmount, 0); const hasTrackStock = linesWithLedger.some((l) => l.trackStock); let status; if (totalPositions === 0) { status = 'empty'; } else if (bookedPositions === totalPositions && bookedQuantity >= totalQuantity) { status = 'booked'; } else if (bookedPositions > 0 || bookedQuantity > 0) { status = 'partial'; } else { status = 'unbooked'; } return { deliveryId, delivery, status, totalPositions, bookedPositions, totalQuantity, bookedQuantity, hasTrackStock, lines: linesWithLedger, }; } /** * Books specified delivery lines to the ledger as goods receipts via /stock/accept. * * @param {Object} params * @param {Object} params.delivery - The delivery object (used for transaction memo and fallback) * @param {Array<{ manifestarticleId?: number, id?: number, amount: number, sku?: string, trackStock?: boolean }>} params.linesToBook * @param {number|string} [params.warehouseId] - Warehouse ID (mandatory only when at least one line tracks stock) * @param {Object} [params.stockApi] * @param {string} [params.transactionText] * @returns {Promise>} */ export async function bookDeliveryLines({ delivery, linesToBook, warehouseId, stockApi = defaultStockApi, transactionText, }) { if (!Array.isArray(linesToBook) || linesToBook.length === 0) { return []; } const numericWarehouseId = Number(warehouseId) || null; const hasTrackedLines = linesToBook.some((l) => l.trackStock); if (hasTrackedLines && (!numericWarehouseId || numericWarehouseId <= 0)) { throw new Error('Bitte wählen Sie ein Lager aus.'); } const memo = String( transactionText != null && transactionText !== '' ? transactionText : `Lieferung ${delivery?.number || delivery?.id || ''}` ).trim(); const items = linesToBook .map((item) => ({ manifestarticleId: Number(item.manifestarticleId ?? item.id), warehouseId: numericWarehouseId, text: memo, })) .filter((item) => item.manifestarticleId && Number.isInteger(item.manifestarticleId) && item.manifestarticleId > 0); if (items.length === 0) { return []; } const res = await stockApi.accept({ items }); for (const item of linesToBook) { if (item.sku) { void artikelAutomat.actions.reloadArticle(item.sku).catch(() => {}); } } return Array.isArray(res?.accepted) ? res.accepted : (res ? [res] : []); }