fix(warehouses): exit-paper truck weight from summed container VGM

Per-truck weight now sums the departing truck's container vgm_tons
(recorded net for bulk); weighed gross only as fallback.
This commit is contained in:
Hagernesh
2026-07-21 14:59:00 +00:00
parent 7372ecbbd8
commit fd3212f326

View File

@@ -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`,
],