/** * Bulk creation of M_Locator rows for one warehouse (Auto-Generate, Warehouse Builder * "create missing", import wizard). Dedupes by X|Y|Z against the warehouse's existing rows, * POSTs with small concurrency, never throws per row (collects `failed`). * Write shape = the proven lowercase one from locatorCatalog.ensureLocator. */ import fetchHelper from './fetchHelper' import pLimit from './pLimit' import { bustLocatorCache, loadWarehouseLocators } from './warehouseLayout' import { xyzKey } from '../../app/utils/warehouseLayout/wiring' export interface BulkLocatorItem { x: string; y: string; z: string; value?: string; priorityNo?: number; locatorTypeId?: number | null } export interface BulkCreateOptions { warehouseId: number organizationId: number items: BulkLocatorItem[] priorityNo?: number isActive?: boolean locatorTypeId?: number | null concurrency?: number } export interface BulkCreateResult { created: Array<{ id: number; value: string; x: string; y: string; z: string }> skipped: Array<{ value: string; reason: string }> failed: Array<{ value: string; message: string }> totalRequested: number } export const MAX_BULK_ITEMS = 500 export async function bulkCreateLocators (event: any, token: string, opts: BulkCreateOptions): Promise { const result: BulkCreateResult = { created: [], skipped: [], failed: [], totalRequested: opts.items.length } const existing = await loadWarehouseLocators(event, token, opts.warehouseId, true) const have = new Set(existing.records.map(r => xyzKey(r.x, r.y, r.z))) const haveValue = new Set(existing.records.map(r => String(r.value).trim().toLowerCase())) const seen = new Set() const todo: BulkLocatorItem[] = [] for (const raw of opts.items.slice(0, MAX_BULK_ITEMS)) { const x = String(raw.x ?? '').trim(), y = String(raw.y ?? '').trim(), z = String(raw.z ?? '').trim() const value = String(raw.value || `${x}-${y}-${z}`).trim() if (!x || !y || !z) { result.skipped.push({ value, reason: 'incomplete' }); continue } const k = xyzKey(x, y, z) if (seen.has(k)) { result.skipped.push({ value, reason: 'duplicate in request' }); continue } seen.add(k) if (have.has(k) || haveValue.has(value.toLowerCase())) { result.skipped.push({ value, reason: 'exists' }); continue } todo.push({ x, y, z, value, priorityNo: raw.priorityNo, locatorTypeId: raw.locatorTypeId }) } const limit = pLimit(opts.concurrency ?? 4) await Promise.all(todo.map(it => limit(async () => { const payload: any = { AD_Org_ID: { id: Number(opts.organizationId), tableName: 'AD_Org' }, M_Warehouse_ID: { id: Number(opts.warehouseId), tableName: 'M_Warehouse' }, value: it.value, x: it.x, y: it.y, z: it.z, priorityNo: Number(it.priorityNo ?? opts.priorityNo ?? 50) || 50, isActive: opts.isActive !== false, isDefault: false } const lt = it.locatorTypeId ?? opts.locatorTypeId if (lt) payload.M_LocatorType_ID = { id: Number(lt), tableName: 'M_LocatorType' } try { const res: any = await fetchHelper(event, 'models/m_locator', 'POST', token, payload) if (res?.id) result.created.push({ id: Number(res.id), value: it.value!, x: it.x, y: it.y, z: it.z }) else result.failed.push({ value: it.value!, message: 'no id returned' }) } catch (err: any) { result.failed.push({ value: it.value!, message: String(err?.data?.detail ?? err?.data?.message ?? err?.message ?? err).slice(0, 300) }) } }))) bustLocatorCache(opts.warehouseId) return result }