diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index b5df563d9..1a371cd60 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -3347,6 +3347,17 @@ export class WarehouseInventoryService { } } + /** + * customer_truck_assignments.gross_weight_kg holds TONNES for gate-out + * recorded exits but real KG for legacy departTruck rows. Exit papers always + * print tonnes — normalise on read. + */ + // ponytail: >1000 heuristic (no truck hauls 1000+ t, no weighbridge reads <1000 kg); + // migrate the column to tonnes if it ever bites. + private grossAsTons(value: number): number { + return value > 1000 ? Math.round(value) / 1000 : value; + } + async releaseDocument(id: string): Promise<{ filename: string; buffer: Buffer }> { const [row] = await this.dataSource.query( `SELECT inv.id, @@ -3406,7 +3417,40 @@ export class WarehouseInventoryService { grossWeightKg: string | number | null; departedAt: string | null; } | null = null; - if (row?.tradeDirection === 'IMPORT' && row?.containerNumber && row?.bookingId) { + // The exit-inspection note written at gate-out names the truck doing THIS + // exit — resolve by its plate first. The item's own container may not be on + // the departing truck at all (trucks pick containers freely per trip). + const notePlates = [...String(row?.notes ?? '').matchAll(/Truck Plate:\s*(\S+)/gi)]; + const exitPlate = notePlates.length ? notePlates[notePlates.length - 1][1] : null; + if (row?.tradeDirection === 'IMPORT' && row?.bookingId && exitPlate) { + const [truckRow] = await this.dataSource.query( + `SELECT a.plate_number AS "plateNumber", + a.driver_name AS "driverName", + a.truck_type AS "truckType", + a.gross_weight_kg AS "grossWeightKg", + a.departed_at AS "departedAt", + string_agg(DISTINCT c.container_number, ', ' ORDER BY c.container_number) AS "containerNumbers", + COALESCE(( + SELECT SUM(bcu.vgm_tons) + FROM freight.customer_truck_containers cc + JOIN freight.booking_container_units bcu + ON bcu.container_number = cc.container_number AND bcu.deleted_at IS NULL + JOIN freight.booking_container bc + ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL + AND bc.booking_id = a.booking_id + WHERE cc.assignment_id = a.id AND cc.deleted_at IS NULL + ), 0) AS "truckWeightTons" + FROM freight.customer_truck_assignments a + LEFT JOIN freight.customer_truck_containers c + ON c.assignment_id = a.id AND c.deleted_at IS NULL + WHERE a.booking_id = $1 AND UPPER(a.plate_number) = UPPER($2) AND a.deleted_at IS NULL + GROUP BY a.id, a.plate_number, a.driver_name, a.truck_type, a.gross_weight_kg, a.departed_at + LIMIT 1`, + [row.bookingId, exitPlate], + ); + truck = truckRow ?? null; + } + if (!truck && row?.tradeDirection === 'IMPORT' && row?.containerNumber && row?.bookingId) { const [truckRow] = await this.dataSource.query( `SELECT a.plate_number AS "plateNumber", a.driver_name AS "driverName", @@ -3436,6 +3480,26 @@ export class WarehouseInventoryService { ); truck = truckRow ?? null; } + // Bulk self-haul (no container to match) or an unmatched container: the exit + // paper is still PER TRUCK — use the latest departed customer truck and its + // weighed gross, never the booking's declared total. + if (!truck && row?.tradeDirection === 'IMPORT' && row?.bookingId) { + const [truckRow] = await this.dataSource.query( + `SELECT a.plate_number AS "plateNumber", + a.driver_name AS "driverName", + a.truck_type AS "truckType", + a.gross_weight_kg AS "grossWeightKg", + a.departed_at AS "departedAt", + NULL AS "containerNumbers", + a.net_weight_tons AS "truckWeightTons" + FROM freight.customer_truck_assignments a + WHERE a.booking_id = $1 AND a.deleted_at IS NULL AND a.departed_at IS NOT NULL + ORDER BY a.departed_at DESC + LIMIT 1`, + [row.bookingId], + ); + truck = truckRow ?? null; + } const bookingReference = row?.bookingReference || 'N/A'; const reference = @@ -3450,7 +3514,9 @@ export class WarehouseInventoryService { customerName: row?.customerName ?? null, freightType: row?.freightType ?? null, tradeDirection: row?.tradeDirection ?? null, - containerNumber: row?.containerNumber ?? null, + // Per-truck exit: list every container leaving on THIS truck, not just + // the inventory item's own container. + containerNumber: truck?.containerNumbers ?? row?.containerNumber ?? null, cargoDescription: row?.cargoDescription ?? null, quantity: Number(row?.quantity ?? 0), weight: Number(row?.weight ?? 0), @@ -3464,12 +3530,12 @@ export class WarehouseInventoryService { truckDriverName: truck?.driverName ?? null, truckType: truck?.truckType ?? null, truckGateOut: truck?.departedAt ?? null, - // Prefer the weighed gross captured on departure; fall back to the summed - // container VGM when the truck hasn't been weighed yet. - truckWeightKg: truck - ? Number(truck.grossWeightKg ?? 0) > 0 - ? Number(truck.grossWeightKg) - : Number(truck.truckWeightTons ?? 0) * 1000 + // Per-truck load in tonnes: the summed VGM of the containers on this truck + // (recorded net for bulk); the weighed gross only as fallback. + truckWeightTons: truck + ? Number(truck.truckWeightTons ?? 0) > 0 + ? Number(truck.truckWeightTons) + : this.grossAsTons(Number(truck.grossWeightKg ?? 0)) : null, }); @@ -3642,10 +3708,17 @@ export class WarehouseInventoryService { ); if (inv?.id) await this.invoices.assertClearanceAllowed(inv.id); - const containers: Array<{ containerNumber: string; goods: string | null }> = + const containers: Array<{ containerNumber: string; goods: string | null; vgmTons: string | null }> = await this.dataSource.query( `SELECT c.container_number AS "containerNumber", - COALESCE(ct.cargo_type_name, b.cargo_free_text) AS goods + COALESCE(ct.cargo_type_name, b.cargo_free_text) AS goods, + (SELECT SUM(u.vgm_tons) + FROM freight.booking_container_units u + JOIN freight.booking_container bc + ON bc.id = u.booking_container_id AND bc.deleted_at IS NULL + WHERE u.container_number = c.container_number + AND bc.booking_id = c.booking_id + AND u.deleted_at IS NULL) AS "vgmTons" FROM freight.customer_truck_containers c JOIN freight.bookings b ON b.id = c.booking_id LEFT JOIN freight.cargo_types ct ON ct.id = b.cargo_type_id @@ -3654,6 +3727,9 @@ export class WarehouseInventoryService { [assignmentId], ); + // Truck load in tonnes: summed container VGM; the weighed gross only as + // fallback (bulk trucks carry no containers). + const vgmSum = containers.reduce((s, c) => s + (Number(c.vgmTons) || 0), 0); const html = this.buildTruckExitPaperHtml({ reference: `REL-${String(truck.bookingReference).replace(/^BK-?/i, '')}-${truck.plateNumber}`, bookingReference: truck.bookingReference, @@ -3661,7 +3737,7 @@ export class WarehouseInventoryService { plateNumber: truck.plateNumber, driverName: truck.driverName, truckType: truck.truckType, - grossWeightKg: Number(truck.grossWeightKg ?? 0), + grossWeightKg: vgmSum > 0 ? vgmSum : this.grossAsTons(Number(truck.grossWeightKg ?? 0)), gateOut: truck.departedAt, containers, }); @@ -5168,7 +5244,7 @@ export class WarehouseInventoryService { truckDriverName?: string | null; truckType?: string | null; truckGateOut?: string | null; - truckWeightKg?: number | null; + truckWeightTons?: number | null; }): string { const esc = (value: unknown) => String(value ?? '-') @@ -5195,8 +5271,8 @@ export class WarehouseInventoryService { ['Quantity', data.quantity], [ data.truckPlateNumber ? 'Gross Weight (Loaded on Truck)' : 'Declared Weight', - `${(data.truckPlateNumber && data.truckWeightKg - ? data.truckWeightKg + `${(data.truckPlateNumber && data.truckWeightTons + ? data.truckWeightTons : data.weight ).toLocaleString()} t`, ],