230 lines
7.6 KiB
JavaScript
230 lines
7.6 KiB
JavaScript
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<Object>} 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<DeliveryLineLedgerStatus>} 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<DeliveryLedgerSummary>}
|
|
*/
|
|
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<Array<Object>>}
|
|
*/
|
|
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] : []);
|
|
}
|