@things-factory/operato-hub 4.3.770 → 4.3.771

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1263,9 +1263,9 @@ async function assignToInventory(
1263
1263
  const orderInventory = orderProduct.orderInventories
1264
1264
  let productBundle = orderProduct?.productBundle
1265
1265
 
1266
- let sortings: any = []
1267
-
1268
1266
  for (let oiIdx = 0; oiIdx < orderInventory.length; oiIdx++) {
1267
+ let sortings: any = []
1268
+
1269
1269
  if (worksheetPickingAssignment?.value !== 'true') {
1270
1270
  switch (orderInventory[oiIdx].product.pickingStrategy) {
1271
1271
  case 'LIFO':
@@ -1347,7 +1347,7 @@ async function assignToInventory(
1347
1347
  acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
1348
1348
  break
1349
1349
  }
1350
- acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
1350
+ return acc
1351
1351
  },
1352
1352
  {
1353
1353
  query: [],
@@ -1385,6 +1385,20 @@ async function assignToInventory(
1385
1385
  const releaseShelfLifeParamIndex = params.length + 1
1386
1386
  params.push(releaseShelfLifeOverride)
1387
1387
 
1388
+ // Add seed row parameters to avoid SQL injection
1389
+ const seedUomIdx = params.length + 1
1390
+ const seedPackingTypeIdx = params.length + 2
1391
+ const seedPackingSizeIdx = params.length + 3
1392
+ const seedReleaseQtyIdx = params.length + 4
1393
+ const seedReleaseUomValueIdx = params.length + 5
1394
+ params.push(
1395
+ orderInventory[oiIdx].uom,
1396
+ orderInventory[oiIdx].packingType,
1397
+ orderInventory[oiIdx].packingSize,
1398
+ orderInventory[oiIdx].releaseQty,
1399
+ orderInventory[oiIdx].releaseUomValue
1400
+ )
1401
+
1388
1402
  let query = `
1389
1403
  update inventories tgt set locked_qty = coalesce(locked_qty,0) + src.reserve_qty,
1390
1404
  locked_uom_value = coalesce(locked_uom_value,0) + src.reserve_uom_value,
@@ -1404,14 +1418,10 @@ async function assignToInventory(
1404
1418
  ) as available_qty,
1405
1419
  sum(qty - locked_qty - release_qty - unassigned_qty) over (order by sort_seq asc rows between unbounded preceding and current row) as lock_amount
1406
1420
  from (
1407
- SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
1408
- null as unit, '${orderInventory[oiIdx].uom}' as uom, '${
1409
- orderInventory[oiIdx].packingType
1410
- }' as packing_type, '${orderInventory[oiIdx].packingSize}' as packing_size,
1421
+ SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
1422
+ null as unit, $${seedUomIdx} as uom, $${seedPackingTypeIdx} as packing_type, $${seedPackingSizeIdx} as packing_size,
1411
1423
  null as manufacture_year, null as carton_id, 0 as uom_value, 0 as locked_uom_value, 0 as qty, 0 as locked_qty, 0 as unassigned_uom_value, 0 as unassigned_qty, null as created_at,
1412
- ${orderInventory[oiIdx].releaseQty}::numeric as release_qty, ${
1413
- orderInventory[oiIdx].releaseUomValue
1414
- }::numeric as release_uom_value
1424
+ $${seedReleaseQtyIdx}::numeric as release_qty, $${seedReleaseUomValueIdx}::numeric as release_uom_value
1415
1425
  UNION
1416
1426
  (
1417
1427
  SELECT ROW_NUMBER() OVER(PARTITION BY iv.domain_id ORDER BY wiar.rank ${
@@ -1444,56 +1454,59 @@ async function assignToInventory(
1444
1454
  TRUE
1445
1455
  END
1446
1456
  )
1447
- ${queryStrings.query.length > 0 ? `AND ${queryStrings.join(' AND ')}` : ''}
1457
+ ${queryStrings.query.length > 0 ? `AND ${queryStrings.query.join(' AND ')}` : ''}
1448
1458
  ORDER BY wiar.rank ${sortQuery ? ', ' + sortQuery : ''}
1449
1459
  )
1450
1460
  ) dt1
1451
1461
  ) dt2 where case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end > 0
1452
1462
  ) dt3 where sort_seq > 0
1453
- ) src where src.id = tgt.id
1454
- returning
1455
- tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",
1456
- tgt."uom", tgt."packing_type", tgt."packing_size", tgt."manufacture_year",
1463
+ ) src where src.id = tgt.id AND tgt.qty >= coalesce(tgt.locked_qty, 0) + src.reserve_qty
1464
+ returning
1465
+ tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",
1466
+ tgt."uom", tgt."packing_type", tgt."packing_size", tgt."manufacture_year",
1457
1467
  tgt."locked_qty", tgt."uom_value", tgt."locked_uom_value", src."reserve_qty", src."reserve_uom_value";`
1458
1468
 
1459
- let updatedInventories = await tx.getRepository(Inventory).query(query, params)
1469
+ const MAX_ASSIGN_RETRIES = 3
1470
+ const RETRY_DELAY_MS = 10
1471
+ let updatedInventories
1460
1472
 
1461
- console.log({
1462
- params: {
1463
- user: user.id || '',
1464
- domain: domain.id || '',
1465
- bizplace: customerBizplace.id || '',
1466
- packingType: orderInventory[oiIdx].packingType || '',
1467
- packingSize: orderInventory[oiIdx].packingSize || '',
1468
- productId: orderInventory[oiIdx].product.id || ''
1469
- },
1470
- result: updatedInventories[0] || '',
1471
- location: 'add-release-order v1 assignInventory',
1472
- time: new Date()
1473
- })
1473
+ for (let attempt = 1; attempt <= MAX_ASSIGN_RETRIES; attempt++) {
1474
+ await tx.query('SAVEPOINT assign_retry')
1474
1475
 
1475
- let totalAssigned = updatedInventories[0].reduce((acc, inv) => {
1476
- return acc + inv.reserve_qty
1477
- }, 0)
1476
+ updatedInventories = await tx.getRepository(Inventory).query(query, params)
1478
1477
 
1479
- let opReleaseQty = productBundle ? orderInventory[oiIdx].releaseQty : orderProduct.releaseQty
1478
+ const assignedRows = updatedInventories?.[0] || []
1479
+ let totalAssigned = assignedRows.reduce((acc, inv) => {
1480
+ return acc + Number(inv.reserve_qty)
1481
+ }, 0)
1480
1482
 
1481
- // For non-decimal products, round the values before comparison
1482
- if (!orderInventory[oiIdx].product.isInventoryDecimal) {
1483
- opReleaseQty = Math.round(opReleaseQty)
1484
- totalAssigned = Math.round(totalAssigned)
1485
- } else {
1486
- // For decimal products, round to 3 decimal places
1487
- opReleaseQty = Math.round(opReleaseQty * 1000) / 1000
1488
- totalAssigned = Math.round(totalAssigned * 1000) / 1000
1489
- }
1483
+ let opReleaseQty = productBundle ? orderInventory[oiIdx].releaseQty : orderProduct.releaseQty
1484
+
1485
+ // For non-decimal products, round the values before comparison
1486
+ if (!orderInventory[oiIdx].product.isInventoryDecimal) {
1487
+ opReleaseQty = Math.round(opReleaseQty)
1488
+ totalAssigned = Math.round(totalAssigned)
1489
+ } else {
1490
+ // For decimal products, round to 3 decimal places
1491
+ opReleaseQty = Math.round(opReleaseQty * 1000) / 1000
1492
+ totalAssigned = Math.round(totalAssigned * 1000) / 1000
1493
+ }
1494
+
1495
+ if (Math.abs(opReleaseQty - totalAssigned) > 0.001) {
1496
+ await tx.query('ROLLBACK TO SAVEPOINT assign_retry')
1497
+ if (attempt < MAX_ASSIGN_RETRIES) {
1498
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS))
1499
+ continue
1500
+ }
1501
+ // Using small epsilon for float comparison
1502
+ throw new ApiError('E01', 'INSUFFICIENT_STOCK')
1503
+ }
1490
1504
 
1491
- if (Math.abs(opReleaseQty - totalAssigned) > 0.001) {
1492
- // Using small epsilon for float comparison
1493
- throw new ApiError('E01', 'INSUFFICIENT_STOCK')
1505
+ await tx.query('RELEASE SAVEPOINT assign_retry')
1506
+ break
1494
1507
  }
1495
1508
 
1496
- updatedInventories[0].forEach(inv => {
1509
+ ;(updatedInventories?.[0] || []).forEach(inv => {
1497
1510
  let oi = {
1498
1511
  ...orderInventory[oiIdx],
1499
1512
  inventory: { id: inv.id },
@@ -1520,9 +1533,11 @@ async function assignToInventory(
1520
1533
  .createQueryBuilder()
1521
1534
  .update(ProductDetailStock)
1522
1535
  .set({
1523
- unassignedQty: () => `"unassigned_qty" + ${orderInventory[oiIdx].releaseQty}`,
1524
- unassignedUomValue: () => `"unassigned_uom_value" + ${orderInventory[oiIdx].releaseUomValue}`
1536
+ unassignedQty: () => `"unassigned_qty" + :releaseQty::numeric`,
1537
+ unassignedUomValue: () => `"unassigned_uom_value" + :releaseUomValue::numeric`
1525
1538
  })
1539
+ .setParameter('releaseQty', orderInventory[oiIdx].releaseQty)
1540
+ .setParameter('releaseUomValue', orderInventory[oiIdx].releaseUomValue)
1526
1541
  .where({ productDetail: orderInventory[oiIdx].productDetail.id })
1527
1542
  .execute()
1528
1543
  }
@@ -448,10 +448,12 @@ async function removeOrderProducts(tx: EntityManager, orderProductIds: string[],
448
448
  .createQueryBuilder()
449
449
  .update(Inventory)
450
450
  .set({
451
- lockedQty: () => `COALESCE(locked_qty, 0) - ${oiReleaseQty}`,
452
- lockedUomValue: () => `COALESCE(locked_uom_value, 0) - ${oiReleaseUomValue}`,
451
+ lockedQty: () => `GREATEST(COALESCE(locked_qty, 0) - :releaseQty::numeric, 0)`,
452
+ lockedUomValue: () => `GREATEST(COALESCE(locked_uom_value, 0) - :releaseUomValue::numeric, 0)`,
453
453
  updater: orderProductEntity.updater
454
454
  })
455
+ .setParameter('releaseQty', oiReleaseQty)
456
+ .setParameter('releaseUomValue', oiReleaseUomValue)
455
457
  .where('id = :id', { id: inventoryEntity.id })
456
458
  .execute()
457
459
  }
@@ -490,9 +492,11 @@ async function removeOrderProducts(tx: EntityManager, orderProductIds: string[],
490
492
  .createQueryBuilder()
491
493
  .update(ProductDetailStock)
492
494
  .set({
493
- unassignedQty: () => `"unassigned_qty" - ${releaseQty}`,
494
- unassignedUomValue: () => `"unassigned_uom_value" - ${releaseUomValue}`
495
+ unassignedQty: () => `GREATEST("unassigned_qty" - :releaseQty::numeric, 0)`,
496
+ unassignedUomValue: () => `GREATEST("unassigned_uom_value" - :releaseUomValue::numeric, 0)`
495
497
  })
498
+ .setParameter('releaseQty', releaseQty)
499
+ .setParameter('releaseUomValue', releaseUomValue)
496
500
  .where({ productDetail: productDetail.id })
497
501
  .execute()
498
502
  }
@@ -560,9 +564,11 @@ async function updateExistingOrderProduct(
560
564
  .createQueryBuilder()
561
565
  .update(ProductDetailStock)
562
566
  .set({
563
- unassignedQty: () => `"unassigned_qty" + ${qtyDiff}`,
564
- unassignedUomValue: () => `"unassigned_uom_value" + ${uomValueDiff}`
567
+ unassignedQty: () => `"unassigned_qty" + :qtyDiff::numeric`,
568
+ unassignedUomValue: () => `"unassigned_uom_value" + :uomValueDiff::numeric`
565
569
  })
570
+ .setParameter('qtyDiff', qtyDiff)
571
+ .setParameter('uomValueDiff', uomValueDiff)
566
572
  .where({ productDetail: existing.productDetail.id })
567
573
  .execute()
568
574
  }
@@ -634,10 +640,12 @@ async function updateOrderInventoriesForQtyChange(
634
640
  .createQueryBuilder()
635
641
  .update(Inventory)
636
642
  .set({
637
- lockedQty: () => `COALESCE(locked_qty, 0) - ${deductQty}`,
638
- lockedUomValue: () => `COALESCE(locked_uom_value, 0) - ${deductUomValue}`,
643
+ lockedQty: () => `GREATEST(COALESCE(locked_qty, 0) - :deductQty::numeric, 0)`,
644
+ lockedUomValue: () => `GREATEST(COALESCE(locked_uom_value, 0) - :deductUomValue::numeric, 0)`,
639
645
  updater: user
640
646
  })
647
+ .setParameter('deductQty', deductQty)
648
+ .setParameter('deductUomValue', deductUomValue)
641
649
  .where('id = :id', { id: inventory.id })
642
650
  .execute()
643
651
  }
@@ -843,7 +851,6 @@ async function assignAdditionalInventory(
843
851
  acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
844
852
  break
845
853
  }
846
- acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
847
854
  return acc
848
855
  },
849
856
  {
@@ -895,6 +902,14 @@ async function assignAdditionalInventory(
895
902
  const releaseShelfLifeParamIndex = params.length + 1
896
903
  params.push(releaseShelfLifeOverride)
897
904
 
905
+ // Add seed row parameters to avoid SQL injection
906
+ const seedUomIdx = params.length + 1
907
+ const seedPackingTypeIdx = params.length + 2
908
+ const seedPackingSizeIdx = params.length + 3
909
+ const seedReleaseQtyIdx = params.length + 4
910
+ const seedReleaseUomValueIdx = params.length + 5
911
+ params.push(uom, orderProduct.packingType, orderProduct.packingSize, qtyDiff, uomValueDiff)
912
+
898
913
  // Build the sophisticated SQL query with window functions
899
914
  let query = `
900
915
  update inventories tgt set locked_qty = coalesce(locked_qty,0) + src.reserve_qty,
@@ -915,12 +930,10 @@ async function assignAdditionalInventory(
915
930
  ) as available_qty,
916
931
  sum(qty - locked_qty - release_qty - unassigned_qty) over (order by sort_seq asc rows between unbounded preceding and current row) as lock_amount
917
932
  from (
918
- SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
919
- null as unit, '${uom}' as uom, '${orderProduct.packingType}' as packing_type, '${
920
- orderProduct.packingSize
921
- }' as packing_size,
933
+ SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
934
+ null as unit, $${seedUomIdx} as uom, $${seedPackingTypeIdx} as packing_type, $${seedPackingSizeIdx} as packing_size,
922
935
  null as manufacture_year, null as carton_id, 0 as uom_value, 0 as locked_uom_value, 0 as qty, 0 as locked_qty, 0 as unassigned_uom_value, 0 as unassigned_qty, null as created_at,
923
- ${qtyDiff}::numeric as release_qty, ${uomValueDiff}::numeric as release_uom_value
936
+ $${seedReleaseQtyIdx}::numeric as release_qty, $${seedReleaseUomValueIdx}::numeric as release_uom_value
924
937
  UNION
925
938
  (
926
939
  SELECT ROW_NUMBER() OVER(PARTITION BY iv.domain_id ORDER BY wiar.rank ${
@@ -959,33 +972,49 @@ async function assignAdditionalInventory(
959
972
  ) dt1
960
973
  ) dt2 where case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end > 0
961
974
  ) dt3 where sort_seq > 0
962
- ) src where src.id = tgt.id
963
- returning
964
- tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",
965
- tgt."uom", tgt."packing_type", tgt."packing_size", tgt."manufacture_year",
975
+ ) src where src.id = tgt.id AND tgt.qty >= coalesce(tgt.locked_qty, 0) + src.reserve_qty
976
+ returning
977
+ tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",
978
+ tgt."uom", tgt."packing_type", tgt."packing_size", tgt."manufacture_year",
966
979
  tgt."locked_qty", tgt."uom_value", tgt."locked_uom_value", src."reserve_qty", src."reserve_uom_value";`
967
980
 
968
- let updatedInventories = await tx.getRepository(Inventory).query(query, params)
981
+ const MAX_ASSIGN_RETRIES = 3
982
+ const RETRY_DELAY_MS = 10
983
+ let updatedInventories
969
984
 
970
- let totalAssigned =
971
- updatedInventories[0]?.reduce((acc: number, inv: any) => {
972
- return acc + inv.reserve_qty
973
- }, 0) || 0
985
+ for (let attempt = 1; attempt <= MAX_ASSIGN_RETRIES; attempt++) {
986
+ await tx.query('SAVEPOINT assign_retry')
974
987
 
975
- // For non-decimal products, round the values before comparison
976
- let roundedQtyDiff = qtyDiff
977
- if (!product.isInventoryDecimal) {
978
- roundedQtyDiff = Math.round(qtyDiff)
979
- totalAssigned = Math.round(totalAssigned)
980
- } else {
981
- // For decimal products, round to 3 decimal places
982
- roundedQtyDiff = Math.round(qtyDiff * 1000) / 1000
983
- totalAssigned = Math.round(totalAssigned * 1000) / 1000
984
- }
988
+ updatedInventories = await tx.getRepository(Inventory).query(query, params)
989
+
990
+ let totalAssigned =
991
+ updatedInventories[0]?.reduce((acc: number, inv: any) => {
992
+ return acc + Number(inv.reserve_qty)
993
+ }, 0) || 0
994
+
995
+ // For non-decimal products, round the values before comparison
996
+ let roundedQtyDiff = qtyDiff
997
+ if (!product.isInventoryDecimal) {
998
+ roundedQtyDiff = Math.round(qtyDiff)
999
+ totalAssigned = Math.round(totalAssigned)
1000
+ } else {
1001
+ // For decimal products, round to 3 decimal places
1002
+ roundedQtyDiff = Math.round(qtyDiff * 1000) / 1000
1003
+ totalAssigned = Math.round(totalAssigned * 1000) / 1000
1004
+ }
1005
+
1006
+ if (Math.abs(roundedQtyDiff - totalAssigned) > 0.001) {
1007
+ await tx.query('ROLLBACK TO SAVEPOINT assign_retry')
1008
+ if (attempt < MAX_ASSIGN_RETRIES) {
1009
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS))
1010
+ continue
1011
+ }
1012
+ // Using small epsilon for float comparison
1013
+ throw new ApiError('E01', 'INSUFFICIENT_STOCK')
1014
+ }
985
1015
 
986
- if (Math.abs(roundedQtyDiff - totalAssigned) > 0.001) {
987
- // Using small epsilon for float comparison
988
- throw new ApiError('E01', 'INSUFFICIENT_STOCK')
1016
+ await tx.query('RELEASE SAVEPOINT assign_retry')
1017
+ break
989
1018
  }
990
1019
 
991
1020
  // Create order inventory records for each assigned inventory
@@ -1225,9 +1254,11 @@ async function addNewOrderProduct(
1225
1254
  .createQueryBuilder()
1226
1255
  .update(ProductDetailStock)
1227
1256
  .set({
1228
- unassignedQty: () => `"unassigned_qty" + ${roundedReleaseQty}`,
1229
- unassignedUomValue: () => `"unassigned_uom_value" + ${releaseUomValue}`
1257
+ unassignedQty: () => `"unassigned_qty" + :releaseQty::numeric`,
1258
+ unassignedUomValue: () => `"unassigned_uom_value" + :releaseUomValue::numeric`
1230
1259
  })
1260
+ .setParameter('releaseQty', roundedReleaseQty)
1261
+ .setParameter('releaseUomValue', releaseUomValue)
1231
1262
  .where({ productDetail: productDetail.id })
1232
1263
  .execute()
1233
1264
  }
@@ -1281,6 +1281,7 @@ async function assignToInventory(orderProducts: OrderProduct[], customerBizplace
1281
1281
 
1282
1282
  let queryFilters = []
1283
1283
 
1284
+ // TODO: Fix reducer bugs — typo itm.filtes -> itm.filters, duplicate acc.query.push, missing return acc
1284
1285
  let queryStrings = queryFilters.reduce(
1285
1286
  (acc, itm, idx, arr) => {
1286
1287
  acc.values.push(itm.filters)
@@ -1332,7 +1333,9 @@ async function assignToInventory(orderProducts: OrderProduct[], customerBizplace
1332
1333
  qty - locked_qty as available_qty,
1333
1334
  sum(qty - locked_qty - release_qty) over (order by sort_seq asc rows between unbounded preceding and current row) as lock_amount
1334
1335
  from (
1335
- SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
1336
+ // TODO: Replace template literal interpolation with $N positional parameters (see unstable/add-release-order.ts for pattern)
1337
+ // String values (uom, packingType, packingSize) are SQL injection vectors; numeric values need ::numeric casts
1338
+ SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
1336
1339
  null as unit, '${orderInventory.uom}' as uom, '${orderInventory.packingType}' as packing_type, '${
1337
1340
  orderInventory.packingSize
1338
1341
  }' as packing_size,
@@ -1352,12 +1355,14 @@ async function assignToInventory(orderProducts: OrderProduct[], customerBizplace
1352
1355
  WHERE case when coalesce("iv"."locked_qty",0) < 0 then 0 else ("iv"."qty" - coalesce("iv"."locked_qty",0)) end > 0 AND
1353
1356
  "iv"."domain_id" = $2 AND "iv"."bizplace_id" = $3 AND "iv"."packing_type" = $4 AND "iv"."packing_size" = $5 AND "iv"."product_id" = $6 AND "iv"."status" = $7 AND "loc"."type" NOT IN ($8, $9)
1354
1357
  AND "iv"."obsolete" = false AND case when "iv"."expiration_date" is not null and "p"."min_outbound_shelf_life" is not null then CURRENT_DATE < "iv"."expiration_date" - "p"."min_outbound_shelf_life" else true end
1355
- ${queryStrings.query.length > 0 ? `AND ${queryStrings.join(' AND ')}` : ''}
1358
+ ${queryStrings.query.length > 0 ? `AND ${queryStrings.query.join(' AND ')}` : ''}
1356
1359
  ORDER BY ${sortQuery}
1357
1360
  )
1358
1361
  ) dt1
1359
1362
  ) dt2 where case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end > 0
1360
1363
  ) dt3 where sort_seq > 0
1364
+ // TODO: Add over-assignment guard: AND tgt.qty >= coalesce(tgt.locked_qty, 0) + src.reserve_qty
1365
+ // TODO: Add SAVEPOINT retry logic with Number() cast on reserve_qty (see v1/add-release-order.ts for pattern)
1361
1366
  ) src where src.id = tgt.id
1362
1367
  returning
1363
1368
  tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",