|
|
|
|
@@ -7,7 +7,7 @@ import { InterchangeDocumentsService } from '../interchange-documents/interchang
|
|
|
|
|
import type { InterchangeDocument } from '../interchange-documents/entities/interchange-document.entity';
|
|
|
|
|
import { LastMileService } from '../last-mile/last-mile.service';
|
|
|
|
|
import { BulkInspectDto } from './dto/bulk-inspect.dto';
|
|
|
|
|
import { BulkReceiveDto } from './dto/bulk-receive.dto';
|
|
|
|
|
import { BulkReceiveDto, TruckEntranceDto } from './dto/bulk-receive.dto';
|
|
|
|
|
import { DeliverInventoryDto } from './dto/deliver-inventory.dto';
|
|
|
|
|
import { FilterWarehouseInventoryDto } from './dto/filter-inventory.dto';
|
|
|
|
|
import { InquiryWarehouseInventoryDto } from './dto/inquiry-inventory.dto';
|
|
|
|
|
@@ -199,12 +199,18 @@ export interface EligibleBookingRow {
|
|
|
|
|
weight: string | null;
|
|
|
|
|
paymentStatus: string;
|
|
|
|
|
status: string;
|
|
|
|
|
hasFirstMile: boolean;
|
|
|
|
|
firstMileRequestId: string | null;
|
|
|
|
|
firstMileStatus: string | null;
|
|
|
|
|
firstMileVehicleId: string | null;
|
|
|
|
|
firstMileTruckPlateNumber: string | null;
|
|
|
|
|
firstMileTrailerPlateNumber: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BulkReceiveResult {
|
|
|
|
|
receivedCount: number;
|
|
|
|
|
skippedCount: number;
|
|
|
|
|
results: { bookingId: string; status: string; inventoryId?: string; reason?: string }[];
|
|
|
|
|
results: { bookingId: string; status: string; inventoryId?: string; grnNumber?: string; reason?: string }[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface LoadPassedExportResult {
|
|
|
|
|
@@ -646,13 +652,29 @@ export class WarehouseInventoryService {
|
|
|
|
|
COALESCE(ct.cargo_type_name, b.cargo_free_text) AS "cargo",
|
|
|
|
|
b.cargo_total_weight_vgm AS "weight",
|
|
|
|
|
b.payment_status AS "paymentStatus",
|
|
|
|
|
b.status AS "status"
|
|
|
|
|
b.status AS "status",
|
|
|
|
|
(NULLIF(TRIM(COALESCE(b.first_mile_pickup_address, '')), '') IS NOT NULL
|
|
|
|
|
OR COALESCE(st.includes_first_mile, false)) AS "hasFirstMile",
|
|
|
|
|
fm.id AS "firstMileRequestId",
|
|
|
|
|
fm.status AS "firstMileStatus",
|
|
|
|
|
fm.vehicle_id AS "firstMileVehicleId",
|
|
|
|
|
v.plate_number AS "firstMileTruckPlateNumber",
|
|
|
|
|
v.trailer_plate_no AS "firstMileTrailerPlateNumber"
|
|
|
|
|
FROM freight.bookings b
|
|
|
|
|
LEFT JOIN freight.companies company ON company.id = b.company_id
|
|
|
|
|
LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
|
|
|
|
|
LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
|
|
|
|
|
LEFT JOIN freight.cargo_types ct ON ct.id = b.cargo_type_id
|
|
|
|
|
LEFT JOIN freight.service_types st ON st.id = b.service_type_id
|
|
|
|
|
LEFT JOIN freight.warehouse_inventory inv ON inv.booking_id = b.id AND inv.deleted_at IS NULL
|
|
|
|
|
LEFT JOIN LATERAL (
|
|
|
|
|
SELECT first_mile.id, first_mile.status, first_mile.vehicle_id
|
|
|
|
|
FROM freight.first_mile first_mile
|
|
|
|
|
WHERE first_mile.booking_id = b.id AND first_mile.deleted_at IS NULL
|
|
|
|
|
ORDER BY first_mile.created_at DESC
|
|
|
|
|
LIMIT 1
|
|
|
|
|
) fm ON true
|
|
|
|
|
LEFT JOIN freight.vehicles v ON v.id = fm.vehicle_id
|
|
|
|
|
WHERE b.deleted_at IS NULL
|
|
|
|
|
AND b.payment_status = 'PAID'
|
|
|
|
|
AND inv.id IS NULL
|
|
|
|
|
@@ -673,6 +695,7 @@ export class WarehouseInventoryService {
|
|
|
|
|
/** Bulk-receive eligible PAID bookings into a location. Skips duplicates / wrong direction. */
|
|
|
|
|
async bulkReceive(dto: BulkReceiveDto): Promise<BulkReceiveResult> {
|
|
|
|
|
const result: BulkReceiveResult = { receivedCount: 0, skippedCount: 0, results: [] };
|
|
|
|
|
this.assertTruckEntrance(dto.truckEntrance);
|
|
|
|
|
|
|
|
|
|
await this.dataSource.transaction(async (manager) => {
|
|
|
|
|
await this.validateLocation(manager, {
|
|
|
|
|
@@ -689,10 +712,22 @@ export class WarehouseInventoryService {
|
|
|
|
|
|
|
|
|
|
const [booking] = await manager.query(
|
|
|
|
|
`SELECT b.payment_status AS "paymentStatus", b.cargo_total_weight_vgm AS "weight",
|
|
|
|
|
oy.country AS "originCountry", dy.country AS "destinationCountry"
|
|
|
|
|
oy.country AS "originCountry", dy.country AS "destinationCountry",
|
|
|
|
|
(NULLIF(TRIM(COALESCE(b.first_mile_pickup_address, '')), '') IS NOT NULL
|
|
|
|
|
OR COALESCE(st.includes_first_mile, false)) AS "hasFirstMile",
|
|
|
|
|
fm.id AS "firstMileRequestId",
|
|
|
|
|
fm.status AS "firstMileStatus"
|
|
|
|
|
FROM freight.bookings b
|
|
|
|
|
LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
|
|
|
|
|
LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
|
|
|
|
|
LEFT JOIN freight.service_types st ON st.id = b.service_type_id
|
|
|
|
|
LEFT JOIN LATERAL (
|
|
|
|
|
SELECT first_mile.id, first_mile.status
|
|
|
|
|
FROM freight.first_mile first_mile
|
|
|
|
|
WHERE first_mile.booking_id = b.id AND first_mile.deleted_at IS NULL
|
|
|
|
|
ORDER BY first_mile.created_at DESC
|
|
|
|
|
LIMIT 1
|
|
|
|
|
) fm ON true
|
|
|
|
|
WHERE b.id = $1 AND b.deleted_at IS NULL LIMIT 1`,
|
|
|
|
|
[bookingId],
|
|
|
|
|
);
|
|
|
|
|
@@ -707,10 +742,28 @@ export class WarehouseInventoryService {
|
|
|
|
|
skip(`Booking route is ${bookingDirection}, not ${dto.direction}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (dto.direction === 'EXPORT' && booking.hasFirstMile) {
|
|
|
|
|
if (!booking.firstMileRequestId) {
|
|
|
|
|
skip('First-mile request not created');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (booking.firstMileStatus !== 'RECEIVED_TO_PORT') {
|
|
|
|
|
skip('First-mile truck has not arrived');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existing = await manager.getRepository(WarehouseInventory).findOne({ where: { bookingId } });
|
|
|
|
|
if (existing) { skip('Already received'); continue; }
|
|
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const grnNumber = this.generateGrnNumber(dto.direction, bookingId, now);
|
|
|
|
|
const receiveNote = this.buildReceiveNote({
|
|
|
|
|
grnNumber,
|
|
|
|
|
direction: dto.direction,
|
|
|
|
|
notes: `Bulk received (${dto.direction})`,
|
|
|
|
|
truckEntrance: dto.truckEntrance,
|
|
|
|
|
});
|
|
|
|
|
const saved = await manager.getRepository(WarehouseInventory).save(
|
|
|
|
|
manager.getRepository(WarehouseInventory).create({
|
|
|
|
|
warehouseId: dto.warehouseId,
|
|
|
|
|
@@ -720,8 +773,8 @@ export class WarehouseInventoryService {
|
|
|
|
|
quantity: 1,
|
|
|
|
|
weight: Number(booking.weight) || 0,
|
|
|
|
|
status: 'RECEIVED',
|
|
|
|
|
arrivedAt: new Date(),
|
|
|
|
|
notes: `Bulk received (${dto.direction})`,
|
|
|
|
|
arrivedAt: now,
|
|
|
|
|
notes: receiveNote,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@@ -730,14 +783,14 @@ export class WarehouseInventoryService {
|
|
|
|
|
activityType: 'INVENTORY_RECEIVED',
|
|
|
|
|
inventoryId: saved.id,
|
|
|
|
|
warehouseId: dto.warehouseId,
|
|
|
|
|
description: `Bulk received ${dto.direction} booking`,
|
|
|
|
|
description: `GRN ${grnNumber}: bulk received ${dto.direction} booking via truck ${dto.truckEntrance.truckPlateNumber}`,
|
|
|
|
|
performedBy: dto.performedBy,
|
|
|
|
|
},
|
|
|
|
|
manager,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
result.receivedCount += 1;
|
|
|
|
|
result.results.push({ bookingId, status: 'RECEIVED', inventoryId: saved.id });
|
|
|
|
|
result.results.push({ bookingId, status: 'RECEIVED', inventoryId: saved.id, grnNumber });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -1430,9 +1483,11 @@ export class WarehouseInventoryService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async receive(dto: ReceiveWarehouseInventoryDto): Promise<WarehouseInventory> {
|
|
|
|
|
this.assertTruckEntrance(dto.truckEntrance);
|
|
|
|
|
const weight = Number(dto.weight) || 0;
|
|
|
|
|
const volume = Number(dto.volume) || 0;
|
|
|
|
|
const containerCount = dto.containerId ? Math.round(Number(dto.quantity) || 0) : 0;
|
|
|
|
|
const bookingDirection = dto.bookingId ? await this.getBookingDirection(dto.bookingId) : null;
|
|
|
|
|
|
|
|
|
|
const id = await this.dataSource.transaction(async (manager) => {
|
|
|
|
|
const { warehouse, yard, zone } = await this.validateLocation(manager, dto);
|
|
|
|
|
@@ -1446,6 +1501,12 @@ export class WarehouseInventoryService {
|
|
|
|
|
this.assertCapacity('Zone', zone, weight, volume, containerCount);
|
|
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const grnNumber = this.generateGrnNumber(bookingDirection ?? 'WH', dto.bookingId ?? 'MANUAL', now);
|
|
|
|
|
const receiveNote = this.buildReceiveNote({
|
|
|
|
|
grnNumber,
|
|
|
|
|
notes: dto.notes?.trim() || 'Single booking received',
|
|
|
|
|
truckEntrance: dto.truckEntrance,
|
|
|
|
|
});
|
|
|
|
|
const saved = await manager.getRepository(WarehouseInventory).save(
|
|
|
|
|
manager.getRepository(WarehouseInventory).create({
|
|
|
|
|
warehouseId: dto.warehouseId,
|
|
|
|
|
@@ -1460,7 +1521,7 @@ export class WarehouseInventoryService {
|
|
|
|
|
volume: dto.volume ?? null,
|
|
|
|
|
status: 'RECEIVED',
|
|
|
|
|
arrivedAt: now,
|
|
|
|
|
notes: dto.notes?.trim() ?? null,
|
|
|
|
|
notes: receiveNote,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@@ -1471,7 +1532,7 @@ export class WarehouseInventoryService {
|
|
|
|
|
activityType: 'INVENTORY_RECEIVED',
|
|
|
|
|
inventoryId: saved.id,
|
|
|
|
|
warehouseId: dto.warehouseId,
|
|
|
|
|
description: `Received ${weight}kg at warehouse location`,
|
|
|
|
|
description: `GRN ${grnNumber}: received ${weight}kg via truck ${dto.truckEntrance.truckPlateNumber}`,
|
|
|
|
|
performedBy: dto.performedBy,
|
|
|
|
|
},
|
|
|
|
|
manager,
|
|
|
|
|
@@ -2349,26 +2410,28 @@ export class WarehouseInventoryService {
|
|
|
|
|
<meta charset="utf-8" />
|
|
|
|
|
<title>Warehouse Gate Clearance / Release Order</title>
|
|
|
|
|
<style>
|
|
|
|
|
body { font-family: "Times New Roman", Georgia, serif; color: #111827; margin: 0; }
|
|
|
|
|
.doc { padding: 10px 8px 0; position: relative; }
|
|
|
|
|
.top { display: grid; grid-template-columns: 1fr auto; gap: 24px; border-bottom: 2px solid #14532d; padding-bottom: 14px; }
|
|
|
|
|
.brand { font-size: 13px; color: #14532d; text-transform: uppercase; letter-spacing: .1em; font-weight: 700; }
|
|
|
|
|
h1 { margin: 7px 0 0; font-size: 27px; line-height: 1.1; text-transform: uppercase; letter-spacing: .03em; }
|
|
|
|
|
.subtitle { margin-top: 6px; font-size: 12px; color: #475569; text-transform: uppercase; letter-spacing: .08em; }
|
|
|
|
|
.ref { text-align: right; font-size: 12px; color: #475569; min-width: 210px; }
|
|
|
|
|
.ref strong { display: block; color: #111827; font-size: 17px; margin: 4px 0 8px; }
|
|
|
|
|
.seal { position: absolute; right: 8px; top: -42px; width: 112px; height: 112px; border: 4px double #14532d; border-radius: 999px; color: #14532d; display: flex; align-items: center; justify-content: center; text-align: center; font-weight: 800; font-size: 15px; line-height: 1.15; transform: rotate(-13deg); opacity: .86; text-transform: uppercase; }
|
|
|
|
|
.seal::before { content: ""; position: absolute; inset: 10px; border: 1px solid #14532d; border-radius: inherit; }
|
|
|
|
|
.notice { margin: 20px 154px 18px 0; padding: 13px 15px; background: #f0fdf4; border: 1px solid #86efac; border-left: 5px solid #14532d; font-size: 13px; line-height: 1.45; }
|
|
|
|
|
.section-title { margin: 18px 0 8px; font-size: 13px; font-weight: 800; color: #14532d; text-transform: uppercase; letter-spacing: .08em; }
|
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
@page { size: A4; margin: 12mm 15mm 14mm; }
|
|
|
|
|
body { font-family: "Times New Roman", Georgia, serif; color: #061323; margin: 0; background: #fff; }
|
|
|
|
|
.doc { position: relative; padding: 0; }
|
|
|
|
|
.top { display: grid; grid-template-columns: 1fr 190px; gap: 26px; border-top: 5px solid #2a2a2a; padding-top: 18px; }
|
|
|
|
|
.brand { font-size: 12px; color: #064c27; text-transform: uppercase; letter-spacing: .13em; font-weight: 800; }
|
|
|
|
|
h1 { margin: 8px 0 0; max-width: 360px; font-size: 29px; line-height: .98; text-transform: uppercase; letter-spacing: .02em; }
|
|
|
|
|
.subtitle { margin-top: 12px; font-size: 11px; color: #3d516a; text-transform: uppercase; letter-spacing: .14em; }
|
|
|
|
|
.ref { text-align: right; font-size: 11px; color: #334155; padding-top: 8px; }
|
|
|
|
|
.ref strong { display: block; color: #061323; font-size: 18px; margin: 5px 0 8px; letter-spacing: .02em; }
|
|
|
|
|
.rule { height: 3px; background: #064c27; margin: 16px 0 22px; }
|
|
|
|
|
.notice { width: 74%; margin: 0 0 18px; padding: 13px 18px; background: #f3fff6; border: 1px solid #61d98b; border-left: 5px solid #16743d; font-size: 13px; line-height: 1.45; }
|
|
|
|
|
.section-title { margin: 18px 0 8px; font-size: 13px; font-weight: 800; color: #064c27; text-transform: uppercase; letter-spacing: .12em; }
|
|
|
|
|
table { width: 100%; border-collapse: collapse; }
|
|
|
|
|
th { width: 31%; text-align: left; color: #334155; background: #f8fafc; font-weight: 700; }
|
|
|
|
|
th, td { border: 1px solid #cbd5e1; padding: 8px 10px; font-size: 12.5px; vertical-align: top; }
|
|
|
|
|
.clause { margin-top: 16px; border: 1px solid #cbd5e1; padding: 12px 14px; font-size: 12.5px; line-height: 1.45; }
|
|
|
|
|
.signatures { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 42px; }
|
|
|
|
|
.officer-signature { position: relative; min-height: 98px; padding-right: 132px; }
|
|
|
|
|
.line { border-top: 1px solid #111827; padding-top: 8px; font-size: 12px; color: #334155; }
|
|
|
|
|
.footer { margin-top: 22px; border-top: 1px solid #cbd5e1; padding-top: 9px; font-size: 10.5px; color: #475569; line-height: 1.45; }
|
|
|
|
|
th { width: 31%; text-align: left; color: #0f2744; background: #f8fafc; font-weight: 800; }
|
|
|
|
|
th, td { border: 1px solid #b9c7d8; padding: 8px 10px; font-size: 12.2px; vertical-align: top; }
|
|
|
|
|
.clause { margin-top: 14px; border: 1px solid #b9c7d8; padding: 12px 15px; font-size: 12.2px; line-height: 1.45; }
|
|
|
|
|
.signatures { display: grid; grid-template-columns: 1fr 96px 1.45fr; gap: 22px; align-items: start; margin-top: 42px; }
|
|
|
|
|
.line { border-top: 1.4px solid #061323; padding-top: 7px; font-size: 10.8px; color: #24384f; min-height: 42px; }
|
|
|
|
|
.seal { width: 96px; height: 96px; margin: -28px auto 0; border: 3px double #17633a; border-radius: 999px; color: #17633a; display: flex; align-items: center; justify-content: center; text-align: center; font-weight: 800; font-size: 14px; line-height: 1.05; transform: rotate(-17deg); text-transform: uppercase; }
|
|
|
|
|
.seal::before { content: ""; position: absolute; width: 78px; height: 78px; border: 1px solid #17633a; border-radius: 999px; }
|
|
|
|
|
.seal span { position: relative; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
@@ -2385,6 +2448,7 @@ export class WarehouseInventoryService {
|
|
|
|
|
Issued: ${esc(issuedAt)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="rule"></div>
|
|
|
|
|
<div class="notice">
|
|
|
|
|
This clearance document confirms that the listed booking/goods are authorized for warehouse exit, subject to gate identity verification and confirmation that no blocking warehouse fees remain unpaid.
|
|
|
|
|
</div>
|
|
|
|
|
@@ -2400,15 +2464,10 @@ export class WarehouseInventoryService {
|
|
|
|
|
cargo details, clearance status, and payment records before permitting exit from the warehouse premises.
|
|
|
|
|
</div>
|
|
|
|
|
<div class="signatures">
|
|
|
|
|
<div class="officer-signature">
|
|
|
|
|
<div class="seal">EDR<br />Warehouse<br />Cleared</div>
|
|
|
|
|
<div class="line">Officer in charge name / signature / date</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="line">Officer in charge name / signature / date</div>
|
|
|
|
|
<div class="seal"><span>EDR<br />Warehouse<br />Cleared</span></div>
|
|
|
|
|
<div class="line">Customer or driver name / signature / date</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="footer">
|
|
|
|
|
Present this original release order at the warehouse gate. This document is valid only for the booking/goods stated above and must be retained or recorded by gate operations according to warehouse procedure.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>`;
|
|
|
|
|
@@ -2448,6 +2507,50 @@ export class WarehouseInventoryService {
|
|
|
|
|
return trimmed ? `${trimmed}\n${note}` : note;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private assertTruckEntrance(truckEntrance?: TruckEntranceDto): void {
|
|
|
|
|
if (!truckEntrance?.truckPlateNumber?.trim()) {
|
|
|
|
|
throw new BadRequestException('Truck plate number is required for entrance registration');
|
|
|
|
|
}
|
|
|
|
|
if (!truckEntrance.driverName?.trim()) {
|
|
|
|
|
throw new BadRequestException('Driver name is required for entrance registration');
|
|
|
|
|
}
|
|
|
|
|
if (!truckEntrance.driverPhone?.trim()) {
|
|
|
|
|
throw new BadRequestException('Driver phone is required for entrance registration');
|
|
|
|
|
}
|
|
|
|
|
if (truckEntrance.entranceTareWeightKg === undefined || Number(truckEntrance.entranceTareWeightKg) < 0) {
|
|
|
|
|
throw new BadRequestException('Entrance tare weight is required for entrance registration');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private generateGrnNumber(direction: string, referenceId: string, date: Date): string {
|
|
|
|
|
const stamp = date.toISOString().slice(0, 10).replace(/-/g, '');
|
|
|
|
|
const suffix = referenceId.replace(/-/g, '').slice(0, 8).toUpperCase();
|
|
|
|
|
return `GRN-${direction.toUpperCase()}-${stamp}-${suffix}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private buildReceiveNote(input: {
|
|
|
|
|
grnNumber: string;
|
|
|
|
|
direction?: string | null;
|
|
|
|
|
notes?: string | null;
|
|
|
|
|
truckEntrance: TruckEntranceDto;
|
|
|
|
|
}): string {
|
|
|
|
|
const truck = input.truckEntrance;
|
|
|
|
|
const rows = [
|
|
|
|
|
`GRN Number: ${input.grnNumber}`,
|
|
|
|
|
input.direction ? `Direction: ${input.direction}` : null,
|
|
|
|
|
`Truck Plate: ${truck.truckPlateNumber}`,
|
|
|
|
|
truck.trailerPlateNumber ? `Trailer Plate: ${truck.trailerPlateNumber}` : null,
|
|
|
|
|
truck.truckType ? `Truck Type: ${truck.truckType}` : null,
|
|
|
|
|
`Driver: ${truck.driverName}`,
|
|
|
|
|
`Driver Phone: ${truck.driverPhone}`,
|
|
|
|
|
truck.driverLicenseNumber ? `Driver License: ${truck.driverLicenseNumber}` : null,
|
|
|
|
|
`Entrance Tare Weight: ${Number(truck.entranceTareWeightKg)} kg`,
|
|
|
|
|
truck.exitTareWeightKg !== undefined ? `Exit Tare Weight: ${Number(truck.exitTareWeightKg)} kg` : null,
|
|
|
|
|
input.notes?.trim() ? `Remarks: ${input.notes.trim()}` : null,
|
|
|
|
|
];
|
|
|
|
|
return rows.filter(Boolean).join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getInventoryAllocationCriteria(item: WarehouseInventory): Promise<InventoryAllocationCriteria> {
|
|
|
|
|
const fallbackFreightType = item.containerId ? 'CONTAINER' : item.cargoId ? 'BULK' : null;
|
|
|
|
|
|
|
|
|
|
|