@things-factory/operato-hub 4.3.744 → 4.3.746
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.
- package/dist-server/routers/api/restful-apis/v1/company/add-contact-points.js +71 -2
- package/dist-server/routers/api/restful-apis/v1/company/add-contact-points.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/company/index.js +1 -0
- package/dist-server/routers/api/restful-apis/v1/company/index.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/company/update-contact-points.js +243 -0
- package/dist-server/routers/api/restful-apis/v1/company/update-contact-points.js.map +1 -0
- package/dist-server/routers/api/restful-apis/v1/utils/params.js +109 -11
- package/dist-server/routers/api/restful-apis/v1/utils/params.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/warehouse/add-inbound-order.js +2 -0
- package/dist-server/routers/api/restful-apis/v1/warehouse/add-inbound-order.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/warehouse/index.js +1 -0
- package/dist-server/routers/api/restful-apis/v1/warehouse/index.js.map +1 -1
- package/dist-server/routers/api/restful-apis/v1/warehouse/update-arrival-notice.js +365 -0
- package/dist-server/routers/api/restful-apis/v1/warehouse/update-arrival-notice.js.map +1 -0
- package/dist-server/routers/api/restful-apis/v1/warehouse/update-release-order-details.js +965 -19
- package/dist-server/routers/api/restful-apis/v1/warehouse/update-release-order-details.js.map +1 -1
- package/openapi/v1/contact-point.yaml +266 -0
- package/openapi/v1/inbound.yaml +353 -0
- package/openapi/v1/outbound.yaml +298 -77
- package/package.json +18 -18
- package/server/routers/api/restful-apis/v1/company/add-contact-points.ts +91 -2
- package/server/routers/api/restful-apis/v1/company/index.ts +1 -0
- package/server/routers/api/restful-apis/v1/company/update-contact-points.ts +267 -0
- package/server/routers/api/restful-apis/v1/utils/params.ts +109 -11
- package/server/routers/api/restful-apis/v1/warehouse/add-inbound-order.ts +2 -0
- package/server/routers/api/restful-apis/v1/warehouse/index.ts +1 -0
- package/server/routers/api/restful-apis/v1/warehouse/update-arrival-notice.ts +429 -0
- package/server/routers/api/restful-apis/v1/warehouse/update-release-order-details.ts +1153 -29
|
@@ -1,15 +1,58 @@
|
|
|
1
|
-
import { EntityManager, getConnection } from 'typeorm'
|
|
1
|
+
import { EntityManager, getConnection, getRepository, In, SelectQueryBuilder } from 'typeorm'
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
2
3
|
|
|
3
4
|
import { restfulApiRouter as router } from '@things-factory/api'
|
|
4
5
|
import { User } from '@things-factory/auth-base'
|
|
5
|
-
import { getMyBizplace } from '@things-factory/biz-base'
|
|
6
|
-
import {
|
|
6
|
+
import { Bizplace, ContactPoint, getCompanyBizplace, getMyBizplace } from '@things-factory/biz-base'
|
|
7
|
+
import { Product, ProductDetail } from '@things-factory/product-base'
|
|
8
|
+
import {
|
|
9
|
+
ORDER_INVENTORY_STATUS,
|
|
10
|
+
ORDER_PRODUCT_STATUS,
|
|
11
|
+
ORDER_STATUS,
|
|
12
|
+
ORDER_TYPES,
|
|
13
|
+
OrderNoGenerator,
|
|
14
|
+
OrderInventory,
|
|
15
|
+
OrderProduct,
|
|
16
|
+
ReleaseGood,
|
|
17
|
+
ShippingOrder
|
|
18
|
+
} from '@things-factory/sales-base'
|
|
19
|
+
import { Setting } from '@things-factory/setting-base'
|
|
20
|
+
import { Domain } from '@things-factory/shell'
|
|
21
|
+
import { Inventory, INVENTORY_STATUS, LOCATION_TYPE, ProductDetailStock } from '@things-factory/warehouse-base'
|
|
22
|
+
import {
|
|
23
|
+
Worksheet,
|
|
24
|
+
WorksheetDetail,
|
|
25
|
+
WORKSHEET_STATUS,
|
|
26
|
+
WORKSHEET_TYPE,
|
|
27
|
+
WorksheetNoGenerator
|
|
28
|
+
} from '@things-factory/worksheet-base'
|
|
7
29
|
|
|
8
30
|
import { businessMiddleware, loggingMiddleware, validationMiddleware } from '../middlewares'
|
|
9
31
|
import { ApiError, ApiErrorHandler, throwInternalServerError } from '../utils/error-util'
|
|
10
32
|
|
|
11
33
|
const debug = require('debug')('things-factory:operato-hub:restful-api:v1:update-release-order-details')
|
|
12
34
|
|
|
35
|
+
// Allowed statuses for updates (order information only)
|
|
36
|
+
const ALLOWED_STATUSES_FOR_INFO_UPDATE = [
|
|
37
|
+
ORDER_STATUS.PENDING,
|
|
38
|
+
ORDER_STATUS.PENDING_RECEIVE,
|
|
39
|
+
ORDER_STATUS.PENDING_WORKSHEET,
|
|
40
|
+
ORDER_STATUS.READY_TO_PICK,
|
|
41
|
+
ORDER_STATUS.PICKING,
|
|
42
|
+
ORDER_STATUS.READY_TO_LOAD,
|
|
43
|
+
ORDER_STATUS.LOADING,
|
|
44
|
+
ORDER_STATUS.READY_TO_PACK,
|
|
45
|
+
ORDER_STATUS.PACKING
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
// Allowed statuses for order product updates
|
|
49
|
+
const ALLOWED_STATUSES_FOR_PRODUCT_UPDATE = [
|
|
50
|
+
ORDER_STATUS.PENDING,
|
|
51
|
+
ORDER_STATUS.PENDING_RECEIVE,
|
|
52
|
+
ORDER_STATUS.PENDING_WORKSHEET,
|
|
53
|
+
ORDER_STATUS.READY_TO_PICK
|
|
54
|
+
]
|
|
55
|
+
|
|
13
56
|
router.post(
|
|
14
57
|
'/v1/warehouse/update-release-order-details',
|
|
15
58
|
businessMiddleware,
|
|
@@ -27,11 +70,12 @@ router.post(
|
|
|
27
70
|
|
|
28
71
|
let patch: any = {}
|
|
29
72
|
patch.releaseGood = bodyReq.releaseGood
|
|
73
|
+
patch.orderProducts = bodyReq.orderProducts || []
|
|
30
74
|
|
|
31
75
|
if (bodyReq?.shippingOrder) patch.shippingOrder = bodyReq.shippingOrder
|
|
32
76
|
else patch.shippingOrder = null
|
|
33
77
|
|
|
34
|
-
context.body = await updateReleaseGoodDetails(tx, domain, patch, user)
|
|
78
|
+
context.body = await updateReleaseGoodDetails(tx, domain, patch, user, context)
|
|
35
79
|
})
|
|
36
80
|
} catch (e) {
|
|
37
81
|
if (e instanceof ApiError) ApiErrorHandler(context, e)
|
|
@@ -40,40 +84,57 @@ router.post(
|
|
|
40
84
|
}
|
|
41
85
|
)
|
|
42
86
|
|
|
43
|
-
async function updateReleaseGoodDetails(tx, domain, patch, user) {
|
|
44
|
-
let { releaseGood, shippingOrder } = patch
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
async function updateReleaseGoodDetails(tx: EntityManager, domain: Domain, patch: any, user: User, context: any) {
|
|
88
|
+
let { releaseGood, shippingOrder, orderProducts: inputOrderProducts } = patch
|
|
89
|
+
|
|
47
90
|
// Build where clause: prioritize refOrderId over roId
|
|
48
91
|
let whereClause: any = { domain }
|
|
49
92
|
const refOrderId = releaseGood?.refOrderId
|
|
50
93
|
const hasRefOrderId = refOrderId != null && refOrderId !== ''
|
|
51
|
-
|
|
94
|
+
const roId = releaseGood?.roId
|
|
95
|
+
|
|
52
96
|
if (hasRefOrderId) {
|
|
53
97
|
whereClause.refOrderId = refOrderId
|
|
54
|
-
} else if (
|
|
55
|
-
whereClause.id =
|
|
98
|
+
} else if (roId) {
|
|
99
|
+
whereClause.id = roId
|
|
56
100
|
} else {
|
|
57
101
|
throw new ApiError('E04', 'release order: roId or refOrderId is required')
|
|
58
102
|
}
|
|
59
|
-
|
|
103
|
+
|
|
60
104
|
let foundReleaseGood = await tx.getRepository(ReleaseGood).findOne({
|
|
61
105
|
where: whereClause,
|
|
62
|
-
relations: ['shippingOrder']
|
|
106
|
+
relations: ['bizplace', 'shippingOrder', 'orderProducts', 'orderProducts.product', 'orderProducts.productDetail']
|
|
63
107
|
})
|
|
64
108
|
|
|
65
109
|
if (!foundReleaseGood) {
|
|
66
|
-
const searchId = hasRefOrderId
|
|
67
|
-
? `refOrderId: ${refOrderId}`
|
|
68
|
-
: `roId: ${releaseGood?.roId}`
|
|
110
|
+
const searchId = hasRefOrderId ? `refOrderId: ${refOrderId}` : `roId: ${roId}`
|
|
69
111
|
throw new ApiError('E04', `release order: ${searchId}`)
|
|
70
112
|
}
|
|
71
113
|
|
|
72
|
-
|
|
73
|
-
|
|
114
|
+
// Validate status for order information updates
|
|
115
|
+
const releaseGoodStatus = (foundReleaseGood as ReleaseGood).status
|
|
116
|
+
if (!ALLOWED_STATUSES_FOR_INFO_UPDATE.includes(releaseGoodStatus)) {
|
|
117
|
+
throw new ApiError(
|
|
118
|
+
'E04',
|
|
119
|
+
`Release order status "${releaseGoodStatus}" does not allow updates. Allowed statuses: ${ALLOWED_STATUSES_FOR_INFO_UPDATE.join(
|
|
120
|
+
', '
|
|
121
|
+
)}`
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Validate status for order product updates
|
|
126
|
+
if (inputOrderProducts && inputOrderProducts.length > 0) {
|
|
127
|
+
if (!ALLOWED_STATUSES_FOR_PRODUCT_UPDATE.includes(releaseGoodStatus)) {
|
|
128
|
+
throw new ApiError(
|
|
129
|
+
'E04',
|
|
130
|
+
`Order product updates are not allowed when release order status is "${releaseGoodStatus}". Order products can only be updated when status is: ${ALLOWED_STATUSES_FOR_PRODUCT_UPDATE.join(
|
|
131
|
+
', '
|
|
132
|
+
)}`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
74
135
|
}
|
|
75
136
|
|
|
76
|
-
//
|
|
137
|
+
// Update shipping order if provided
|
|
77
138
|
if (shippingOrder !== null) {
|
|
78
139
|
shippingOrder.remark = shippingOrder.exportRemark
|
|
79
140
|
shippingOrder.containerClosureDateTime = shippingOrder.containerClosureDate
|
|
@@ -82,11 +143,11 @@ async function updateReleaseGoodDetails(tx, domain, patch, user) {
|
|
|
82
143
|
delete shippingOrder.exportRemark
|
|
83
144
|
}
|
|
84
145
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else if (!
|
|
146
|
+
const releaseGoodEntityForShipping = foundReleaseGood as ReleaseGood
|
|
147
|
+
const foundShippingOrder = releaseGoodEntityForShipping.shippingOrder as ShippingOrder | null
|
|
148
|
+
if (foundShippingOrder && shippingOrder) {
|
|
149
|
+
await tx.getRepository(ShippingOrder).update(foundShippingOrder.id, shippingOrder)
|
|
150
|
+
} else if (!foundShippingOrder && shippingOrder) {
|
|
90
151
|
let newShippingOrder: ShippingOrder = await tx.getRepository(ShippingOrder).save({
|
|
91
152
|
...shippingOrder,
|
|
92
153
|
name: OrderNoGenerator.shippingOrder(),
|
|
@@ -100,11 +161,1074 @@ async function updateReleaseGoodDetails(tx, domain, patch, user) {
|
|
|
100
161
|
releaseGood.shippingOrder = newShippingOrder
|
|
101
162
|
}
|
|
102
163
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
164
|
+
// Update order products if provided
|
|
165
|
+
if (inputOrderProducts && inputOrderProducts.length > 0) {
|
|
166
|
+
await updateOrderProducts(tx, foundReleaseGood, inputOrderProducts, domain, user, context)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Update release good fields
|
|
170
|
+
const updateData: any = {
|
|
171
|
+
remark: releaseGood?.remark ? releaseGood.remark : null,
|
|
172
|
+
updater: user
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const releaseGoodPatch = releaseGood as any
|
|
176
|
+
const fieldsToUpdate = [
|
|
177
|
+
'refNo',
|
|
178
|
+
'refNo2',
|
|
179
|
+
'refNo3',
|
|
180
|
+
'description',
|
|
181
|
+
'truckNo',
|
|
182
|
+
'collectionOrderNo',
|
|
183
|
+
'releaseDate',
|
|
184
|
+
'ownTransport',
|
|
185
|
+
'exportOption',
|
|
186
|
+
'packingOption',
|
|
187
|
+
'courierOption',
|
|
188
|
+
'shippingOrder',
|
|
189
|
+
'city',
|
|
190
|
+
'district',
|
|
191
|
+
'state',
|
|
192
|
+
'country',
|
|
193
|
+
'postalCode',
|
|
194
|
+
'phone1',
|
|
195
|
+
'transporter',
|
|
196
|
+
'trackingNo',
|
|
197
|
+
'airwayBill',
|
|
198
|
+
'invoice'
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
fieldsToUpdate.forEach(field => {
|
|
202
|
+
if (releaseGoodPatch?.[field] !== undefined) {
|
|
203
|
+
updateData[field] = releaseGoodPatch[field]
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const releaseGoodEntityForUpdate = foundReleaseGood as ReleaseGood
|
|
208
|
+
// Exclude orderProducts relation to prevent TypeORM from syncing and clearing foreign keys
|
|
209
|
+
const { orderProducts, orderInventories, deliveryOrders, ...releaseGoodWithoutRelations } = releaseGoodEntityForUpdate
|
|
210
|
+
await tx.getRepository(ReleaseGood).save({
|
|
211
|
+
...releaseGoodWithoutRelations,
|
|
212
|
+
...updateData
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// Fetch updated release good with relations for response
|
|
216
|
+
// Query order products separately to ensure we get all of them including newly added ones
|
|
217
|
+
const releaseGoodId = (foundReleaseGood as ReleaseGood).id
|
|
218
|
+
|
|
219
|
+
// Fetch release good basic info
|
|
220
|
+
const updatedReleaseGood: ReleaseGood | undefined = await tx.getRepository(ReleaseGood).findOne({
|
|
221
|
+
where: { id: releaseGoodId },
|
|
222
|
+
relations: ['shippingOrder']
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
if (!updatedReleaseGood) {
|
|
226
|
+
throw new ApiError('E04', 'Failed to retrieve updated release order')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Fetch all order products separately to ensure we get newly added ones
|
|
230
|
+
const allOrderProducts = await tx.getRepository(OrderProduct).find({
|
|
231
|
+
where: { releaseGood: { id: releaseGoodId } },
|
|
232
|
+
relations: ['product', 'productDetail']
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Attach order products to release good
|
|
236
|
+
updatedReleaseGood.orderProducts = allOrderProducts
|
|
237
|
+
|
|
238
|
+
const releaseGoodForResponse = updatedReleaseGood as ReleaseGood
|
|
239
|
+
|
|
240
|
+
// Format response - only include necessary fields
|
|
241
|
+
const resultOrderProducts = (releaseGoodForResponse.orderProducts || []).map((op: OrderProduct) => ({
|
|
242
|
+
id: op.id,
|
|
243
|
+
product: {
|
|
244
|
+
sku: op.product?.sku || null,
|
|
245
|
+
name: op.product?.name || null
|
|
246
|
+
},
|
|
247
|
+
productDetail: {
|
|
248
|
+
refCode: (op.productDetail as any)?.refCode || null
|
|
249
|
+
},
|
|
250
|
+
batchId: op.batchId,
|
|
251
|
+
packingType: op.packingType,
|
|
252
|
+
packingSize: op.packingSize,
|
|
253
|
+
releaseQty: op.releaseQty,
|
|
254
|
+
releaseUomValue: op.releaseUomValue,
|
|
255
|
+
uom: op.uom,
|
|
256
|
+
refItemId: op.refItemId || null,
|
|
257
|
+
unitPrice: op.unitPrice || null,
|
|
258
|
+
remark: op.remark || null,
|
|
259
|
+
status: op.status
|
|
260
|
+
}))
|
|
261
|
+
|
|
262
|
+
const shippingOrderData = releaseGoodForResponse.shippingOrder
|
|
263
|
+
? {
|
|
264
|
+
id: (releaseGoodForResponse.shippingOrder as ShippingOrder).id,
|
|
265
|
+
name: (releaseGoodForResponse.shippingOrder as ShippingOrder).name,
|
|
266
|
+
status: (releaseGoodForResponse.shippingOrder as ShippingOrder).status
|
|
267
|
+
}
|
|
268
|
+
: null
|
|
269
|
+
|
|
270
|
+
const data = {
|
|
271
|
+
id: releaseGoodForResponse.id,
|
|
272
|
+
roNo: releaseGoodForResponse.name,
|
|
273
|
+
refOrderId: releaseGoodForResponse.refOrderId,
|
|
274
|
+
collectionOrderNo: releaseGoodForResponse.collectionOrderNo,
|
|
275
|
+
refNo: releaseGoodForResponse.refNo,
|
|
276
|
+
refNo2: releaseGoodForResponse.refNo2,
|
|
277
|
+
refNo3: releaseGoodForResponse.refNo3,
|
|
278
|
+
description: releaseGoodForResponse.description,
|
|
279
|
+
marketPlaceOrderStatus: releaseGoodForResponse.marketPlaceOrderStatus,
|
|
280
|
+
billingAddress: releaseGoodForResponse.billingAddress,
|
|
281
|
+
deliveryAddress: releaseGoodForResponse.deliveryAddress1,
|
|
282
|
+
attentionTo: releaseGoodForResponse.attentionTo,
|
|
283
|
+
city: releaseGoodForResponse.city,
|
|
284
|
+
district: releaseGoodForResponse.district,
|
|
285
|
+
state: releaseGoodForResponse.state,
|
|
286
|
+
country: releaseGoodForResponse.country,
|
|
287
|
+
postalCode: releaseGoodForResponse.postalCode,
|
|
288
|
+
phone: releaseGoodForResponse.phone1,
|
|
289
|
+
transporter: releaseGoodForResponse.transporter,
|
|
290
|
+
trackingNo: releaseGoodForResponse.trackingNo,
|
|
291
|
+
airwayBill: releaseGoodForResponse.airwayBill,
|
|
292
|
+
invoice: releaseGoodForResponse.invoice,
|
|
293
|
+
truckNo: releaseGoodForResponse.truckNo,
|
|
294
|
+
releaseDate: releaseGoodForResponse.releaseDate,
|
|
295
|
+
ownTransport: releaseGoodForResponse.ownTransport,
|
|
296
|
+
exportOption: releaseGoodForResponse.exportOption,
|
|
297
|
+
packingOption: releaseGoodForResponse.packingOption,
|
|
298
|
+
courierOption: releaseGoodForResponse.courierOption,
|
|
299
|
+
status: releaseGoodForResponse.status,
|
|
300
|
+
remark: releaseGoodForResponse.remark,
|
|
301
|
+
orderProducts: resultOrderProducts,
|
|
302
|
+
shippingOrder: shippingOrderData
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
responseCode: '200',
|
|
307
|
+
message: 'success',
|
|
308
|
+
data
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function updateOrderProducts(
|
|
313
|
+
tx: EntityManager,
|
|
314
|
+
releaseGood: ReleaseGood,
|
|
315
|
+
inputOrderProducts: any[],
|
|
316
|
+
domain: Domain,
|
|
317
|
+
user: User,
|
|
318
|
+
context: any
|
|
319
|
+
) {
|
|
320
|
+
// Get existing order products with their order inventories
|
|
321
|
+
const existingOrderProducts = await tx.getRepository(OrderProduct).find({
|
|
322
|
+
where: { releaseGood },
|
|
323
|
+
relations: ['product', 'productDetail', 'orderInventories', 'orderInventories.inventory']
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Determine hasOrderInventories by checking if releaseGood has any orderInventories
|
|
327
|
+
// Check directly on releaseGood by counting orderInventories
|
|
328
|
+
const orderInventoriesCount = await tx.getRepository(OrderInventory).count({
|
|
329
|
+
where: { releaseGood }
|
|
330
|
+
})
|
|
331
|
+
const hasOrderInventories = orderInventoriesCount > 0
|
|
332
|
+
|
|
333
|
+
// Process removals first
|
|
334
|
+
const orderProductIdsToRemove: string[] = []
|
|
335
|
+
const orderProductsToUpdate: any[] = []
|
|
336
|
+
const orderProductsToAdd: any[] = []
|
|
337
|
+
|
|
338
|
+
for (const orderProductReq of inputOrderProducts) {
|
|
339
|
+
if (orderProductReq._action === 'remove') {
|
|
340
|
+
const matchingOP = findMatchingOrderProduct(existingOrderProducts, orderProductReq)
|
|
341
|
+
if (!matchingOP) {
|
|
342
|
+
throw new ApiError(
|
|
343
|
+
'E04',
|
|
344
|
+
`Order product not found for removal. Provide id, sku, refCode, batchId, packingType, or refItemId to identify the item.`
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
orderProductIdsToRemove.push(matchingOP.id)
|
|
348
|
+
continue
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Try to find existing order product for update
|
|
352
|
+
const matchingOP = findMatchingOrderProduct(existingOrderProducts, orderProductReq)
|
|
353
|
+
|
|
354
|
+
if (matchingOP) {
|
|
355
|
+
orderProductsToUpdate.push({ existing: matchingOP, request: orderProductReq })
|
|
356
|
+
} else {
|
|
357
|
+
orderProductsToAdd.push(orderProductReq)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Validate: Prevent removing all products from the order
|
|
362
|
+
// If removing products would leave the order with zero products, throw an error
|
|
363
|
+
const existingProductCount = existingOrderProducts.length
|
|
364
|
+
const productsToRemoveCount = orderProductIdsToRemove.length
|
|
365
|
+
const productsToAddCount = orderProductsToAdd.length
|
|
366
|
+
const remainingProductCount = existingProductCount - productsToRemoveCount + productsToAddCount
|
|
367
|
+
|
|
368
|
+
if (remainingProductCount <= 0) {
|
|
369
|
+
throw new ApiError(
|
|
370
|
+
'E04',
|
|
371
|
+
'Cannot remove all products from the release order. Please use the cancellation API to cancel the entire order instead of removing the final item(s).'
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Remove order products
|
|
376
|
+
if (orderProductIdsToRemove.length > 0) {
|
|
377
|
+
await removeOrderProducts(tx, orderProductIdsToRemove, hasOrderInventories)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Update existing order products
|
|
381
|
+
for (const { existing, request } of orderProductsToUpdate) {
|
|
382
|
+
await updateExistingOrderProduct(tx, existing, request, releaseGood, domain, user, context, hasOrderInventories)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add new order products
|
|
386
|
+
for (const orderProductReq of orderProductsToAdd) {
|
|
387
|
+
await addNewOrderProduct(tx, orderProductReq, releaseGood, domain, user, context, hasOrderInventories)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function findMatchingOrderProduct(existingOrderProducts: OrderProduct[], orderProductReq: any): OrderProduct | null {
|
|
392
|
+
// First try by ID if provided
|
|
393
|
+
if (orderProductReq.id) {
|
|
394
|
+
const found = existingOrderProducts.find(op => op.id === orderProductReq.id)
|
|
395
|
+
if (found) return found
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Extract matching fields
|
|
399
|
+
const sku = orderProductReq.product?.sku || orderProductReq.sku
|
|
400
|
+
const refCode = orderProductReq.product?.refCode || orderProductReq.productDetail?.refCode || orderProductReq.refCode
|
|
401
|
+
const batchId = orderProductReq.batchId
|
|
402
|
+
const packingType = orderProductReq.packingType
|
|
403
|
+
const refItemId = orderProductReq.refItemId
|
|
404
|
+
|
|
405
|
+
// Need at least one matching field (besides id)
|
|
406
|
+
if (!sku && !refCode && !batchId && !packingType && !refItemId) {
|
|
407
|
+
return null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Match by provided fields (all provided fields must match)
|
|
411
|
+
return (
|
|
412
|
+
existingOrderProducts.find(op => {
|
|
413
|
+
const matchesSku = !sku || op.product?.sku === sku
|
|
414
|
+
const matchesRefCode = !refCode || (op.productDetail as any)?.refCode === refCode
|
|
415
|
+
const matchesBatchId = !batchId || op.batchId === batchId
|
|
416
|
+
const matchesPackingType = !packingType || op.packingType === packingType
|
|
417
|
+
const matchesRefItemId = !refItemId || op.refItemId === refItemId
|
|
418
|
+
|
|
419
|
+
return matchesSku && matchesRefCode && matchesBatchId && matchesPackingType && matchesRefItemId
|
|
420
|
+
}) || null
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function removeOrderProducts(tx: EntityManager, orderProductIds: string[], hasOrderInventories: boolean) {
|
|
425
|
+
for (const orderProductId of orderProductIds) {
|
|
426
|
+
const orderProduct = await tx.getRepository(OrderProduct).findOne({
|
|
427
|
+
where: { id: orderProductId },
|
|
428
|
+
relations: ['orderInventories', 'orderInventories.inventory']
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
if (!orderProduct) continue
|
|
432
|
+
|
|
433
|
+
const orderProductEntity = orderProduct as OrderProduct
|
|
434
|
+
const orderInventories = (orderProductEntity.orderInventories || []) as OrderInventory[]
|
|
435
|
+
const releaseQty = orderProductEntity.releaseQty || 0
|
|
436
|
+
const releaseUomValue = orderProductEntity.releaseUomValue || 0
|
|
437
|
+
|
|
438
|
+
if (hasOrderInventories && orderInventories.length > 0) {
|
|
439
|
+
// Release locked quantities from inventories
|
|
440
|
+
for (const orderInventory of orderInventories) {
|
|
441
|
+
const inventory = orderInventory.inventory as Inventory | null
|
|
442
|
+
if (inventory) {
|
|
443
|
+
const oiReleaseQty = orderInventory.releaseQty || 0
|
|
444
|
+
const oiReleaseUomValue = orderInventory.releaseUomValue || 0
|
|
445
|
+
const inventoryEntity = inventory as Inventory
|
|
446
|
+
await tx
|
|
447
|
+
.getRepository(Inventory)
|
|
448
|
+
.createQueryBuilder()
|
|
449
|
+
.update(Inventory)
|
|
450
|
+
.set({
|
|
451
|
+
lockedQty: () => `COALESCE(locked_qty, 0) - ${oiReleaseQty}`,
|
|
452
|
+
lockedUomValue: () => `COALESCE(locked_uom_value, 0) - ${oiReleaseUomValue}`,
|
|
453
|
+
updater: orderProductEntity.updater
|
|
454
|
+
})
|
|
455
|
+
.where('id = :id', { id: inventoryEntity.id })
|
|
456
|
+
.execute()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Delete order inventories
|
|
461
|
+
// First, delete any worksheet details that reference these order inventories
|
|
462
|
+
const oiIds = orderInventories.map(oi => oi.id).filter((id): id is string => !!id)
|
|
463
|
+
if (oiIds.length > 0) {
|
|
464
|
+
// Find and delete worksheet details that reference the order inventories to be deleted
|
|
465
|
+
// Join with OrderInventory to find worksheet details by order inventory IDs
|
|
466
|
+
const worksheetDetailsToDelete: WorksheetDetail[] = await tx
|
|
467
|
+
.getRepository(WorksheetDetail)
|
|
468
|
+
.createQueryBuilder('wsd')
|
|
469
|
+
.innerJoin('wsd.targetInventory', 'oi')
|
|
470
|
+
.where('oi.id IN (:...oiIds)', { oiIds })
|
|
471
|
+
.andWhere('wsd.domain = :domainId', {
|
|
472
|
+
domainId: orderProductEntity.domain?.id || orderProductEntity.domainId
|
|
473
|
+
})
|
|
474
|
+
.getMany()
|
|
475
|
+
|
|
476
|
+
if (worksheetDetailsToDelete.length > 0) {
|
|
477
|
+
const worksheetDetailIds = worksheetDetailsToDelete.map(wsd => wsd.id)
|
|
478
|
+
await tx.getRepository(WorksheetDetail).delete(worksheetDetailIds)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Now delete the order inventories
|
|
482
|
+
await tx.getRepository(OrderInventory).delete(oiIds)
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
// Update ProductDetailStock unassigned quantities
|
|
486
|
+
const productDetail = orderProductEntity.productDetail as ProductDetail | null
|
|
487
|
+
if (productDetail && productDetail.id) {
|
|
488
|
+
await tx
|
|
489
|
+
.getRepository(ProductDetailStock)
|
|
490
|
+
.createQueryBuilder()
|
|
491
|
+
.update(ProductDetailStock)
|
|
492
|
+
.set({
|
|
493
|
+
unassignedQty: () => `"unassigned_qty" - ${releaseQty}`,
|
|
494
|
+
unassignedUomValue: () => `"unassigned_uom_value" - ${releaseUomValue}`
|
|
495
|
+
})
|
|
496
|
+
.where({ productDetail: productDetail.id })
|
|
497
|
+
.execute()
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Delete order product
|
|
502
|
+
await tx.getRepository(OrderProduct).delete(orderProductId)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function updateExistingOrderProduct(
|
|
507
|
+
tx: EntityManager,
|
|
508
|
+
existing: OrderProduct,
|
|
509
|
+
request: any,
|
|
510
|
+
releaseGood: ReleaseGood,
|
|
511
|
+
domain: Domain,
|
|
512
|
+
user: User,
|
|
513
|
+
context: any,
|
|
514
|
+
hasOrderInventories: boolean
|
|
515
|
+
) {
|
|
516
|
+
const newQty = request.releaseQty !== undefined ? request.releaseQty : request.qty
|
|
517
|
+
if (newQty === undefined) {
|
|
518
|
+
// No quantity change, just update other fields if provided
|
|
519
|
+
const updateData: any = { updater: user }
|
|
520
|
+
if (request.remark !== undefined) updateData.remark = request.remark
|
|
521
|
+
if (request.unitPrice !== undefined) updateData.unitPrice = request.unitPrice
|
|
522
|
+
|
|
523
|
+
await tx.getRepository(OrderProduct).update(existing.id, updateData)
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (newQty <= 0) {
|
|
528
|
+
throw new ApiError('E01', 'releaseQty must be greater than 0')
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const qtyDiff = newQty - existing.releaseQty
|
|
532
|
+
const uomValueDiff = (newQty - existing.releaseQty) * (existing.productDetail?.uomValue || 1)
|
|
533
|
+
|
|
534
|
+
// Validate decimal places for non-decimal products
|
|
535
|
+
if (!existing.product?.isInventoryDecimal && newQty % 1 !== 0) {
|
|
536
|
+
throw new ApiError('E01', `releaseQty must be an integer for product ${existing.product?.sku}`)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Round to 3 decimal places
|
|
540
|
+
const roundedNewQty = existing.product?.isInventoryDecimal ? Math.round(newQty * 1000) / 1000 : Math.round(newQty)
|
|
541
|
+
const roundedNewUomValue = roundedNewQty * (existing.productDetail?.uomValue || 1)
|
|
542
|
+
|
|
543
|
+
if (hasOrderInventories) {
|
|
544
|
+
// Update order inventories
|
|
545
|
+
await updateOrderInventoriesForQtyChange(
|
|
546
|
+
tx,
|
|
547
|
+
existing,
|
|
548
|
+
qtyDiff,
|
|
549
|
+
uomValueDiff,
|
|
550
|
+
roundedNewQty,
|
|
551
|
+
roundedNewUomValue,
|
|
552
|
+
user,
|
|
553
|
+
releaseGood,
|
|
554
|
+
domain
|
|
555
|
+
)
|
|
556
|
+
} else {
|
|
557
|
+
// Update ProductDetailStock
|
|
558
|
+
await tx
|
|
559
|
+
.getRepository(ProductDetailStock)
|
|
560
|
+
.createQueryBuilder()
|
|
561
|
+
.update(ProductDetailStock)
|
|
562
|
+
.set({
|
|
563
|
+
unassignedQty: () => `"unassigned_qty" + ${qtyDiff}`,
|
|
564
|
+
unassignedUomValue: () => `"unassigned_uom_value" + ${uomValueDiff}`
|
|
565
|
+
})
|
|
566
|
+
.where({ productDetail: existing.productDetail.id })
|
|
567
|
+
.execute()
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Update order product
|
|
571
|
+
const updateData: any = {
|
|
572
|
+
releaseQty: roundedNewQty,
|
|
573
|
+
releaseUomValue: roundedNewUomValue,
|
|
574
|
+
updater: user
|
|
575
|
+
}
|
|
576
|
+
if (request.remark !== undefined) updateData.remark = request.remark
|
|
577
|
+
if (request.unitPrice !== undefined) updateData.unitPrice = request.unitPrice
|
|
578
|
+
|
|
579
|
+
await tx.getRepository(OrderProduct).update(existing.id, updateData)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function updateOrderInventoriesForQtyChange(
|
|
583
|
+
tx: EntityManager,
|
|
584
|
+
orderProduct: OrderProduct,
|
|
585
|
+
qtyDiff: number,
|
|
586
|
+
uomValueDiff: number,
|
|
587
|
+
newQty: number,
|
|
588
|
+
newUomValue: number,
|
|
589
|
+
user: User,
|
|
590
|
+
releaseGood: ReleaseGood,
|
|
591
|
+
domain: Domain
|
|
592
|
+
) {
|
|
593
|
+
const existingOrderInventories = (await tx.getRepository(OrderInventory).find({
|
|
594
|
+
where: { orderProduct },
|
|
595
|
+
relations: ['inventory']
|
|
596
|
+
})) as OrderInventory[]
|
|
597
|
+
|
|
598
|
+
if (qtyDiff > 0) {
|
|
599
|
+
// Increase: assign more inventory
|
|
600
|
+
await assignAdditionalInventory(tx, orderProduct, qtyDiff, uomValueDiff, user, releaseGood, domain)
|
|
601
|
+
} else if (qtyDiff < 0) {
|
|
602
|
+
// Decrease: select latest order inventories and deduct from them
|
|
603
|
+
const reductionQty = Math.abs(qtyDiff)
|
|
604
|
+
const reductionUomValue = Math.abs(uomValueDiff)
|
|
605
|
+
|
|
606
|
+
// Sort order inventories by creation date (latest first)
|
|
607
|
+
const sortedOrderInventories = [...existingOrderInventories].sort((a, b) => {
|
|
608
|
+
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0
|
|
609
|
+
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0
|
|
610
|
+
return dateB - dateA // Latest first
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
let remainingReductionQty = reductionQty
|
|
614
|
+
let remainingReductionUomValue = reductionUomValue
|
|
615
|
+
const orderInventoriesToDelete: string[] = []
|
|
616
|
+
|
|
617
|
+
for (const oi of sortedOrderInventories) {
|
|
618
|
+
if (remainingReductionQty <= 0 && remainingReductionUomValue <= 0) {
|
|
619
|
+
break
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const oiReleaseQty = oi.releaseQty || 0
|
|
623
|
+
const oiReleaseUomValue = oi.releaseUomValue || 0
|
|
624
|
+
|
|
625
|
+
// Calculate how much to deduct from this order inventory
|
|
626
|
+
const deductQty = Math.min(remainingReductionQty, oiReleaseQty)
|
|
627
|
+
const deductUomValue = Math.min(remainingReductionUomValue, oiReleaseUomValue)
|
|
628
|
+
|
|
629
|
+
const inventory = oi.inventory as Inventory | null
|
|
630
|
+
if (inventory) {
|
|
631
|
+
// Release locked quantities from inventory
|
|
632
|
+
await tx
|
|
633
|
+
.getRepository(Inventory)
|
|
634
|
+
.createQueryBuilder()
|
|
635
|
+
.update(Inventory)
|
|
636
|
+
.set({
|
|
637
|
+
lockedQty: () => `COALESCE(locked_qty, 0) - ${deductQty}`,
|
|
638
|
+
lockedUomValue: () => `COALESCE(locked_uom_value, 0) - ${deductUomValue}`,
|
|
639
|
+
updater: user
|
|
640
|
+
})
|
|
641
|
+
.where('id = :id', { id: inventory.id })
|
|
642
|
+
.execute()
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const newOiQty = oiReleaseQty - deductQty
|
|
646
|
+
const newOiUomValue = oiReleaseUomValue - deductUomValue
|
|
647
|
+
|
|
648
|
+
if (newOiQty <= 0 || newOiUomValue <= 0) {
|
|
649
|
+
// Mark this order inventory for deletion
|
|
650
|
+
orderInventoriesToDelete.push(oi.id)
|
|
651
|
+
} else {
|
|
652
|
+
// Update order inventory with remaining quantity
|
|
653
|
+
await tx.getRepository(OrderInventory).update(oi.id, {
|
|
654
|
+
releaseQty: newOiQty,
|
|
655
|
+
releaseUomValue: newOiUomValue,
|
|
656
|
+
updater: user
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
remainingReductionQty -= deductQty
|
|
661
|
+
remainingReductionUomValue -= deductUomValue
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Delete order inventories that are fully consumed
|
|
665
|
+
if (orderInventoriesToDelete.length > 0) {
|
|
666
|
+
// Find and delete worksheet details that reference these order inventories
|
|
667
|
+
const worksheetDetailsToDelete: WorksheetDetail[] = await tx
|
|
668
|
+
.getRepository(WorksheetDetail)
|
|
669
|
+
.createQueryBuilder('wsd')
|
|
670
|
+
.innerJoin('wsd.targetInventory', 'oi')
|
|
671
|
+
.where('oi.id IN (:...oiIds)', { oiIds: orderInventoriesToDelete })
|
|
672
|
+
.andWhere('wsd.domain = :domainId', { domainId: domain.id })
|
|
673
|
+
.getMany()
|
|
674
|
+
|
|
675
|
+
if (worksheetDetailsToDelete.length > 0) {
|
|
676
|
+
const worksheetDetailIds = worksheetDetailsToDelete.map(wsd => wsd.id)
|
|
677
|
+
await tx.getRepository(WorksheetDetail).delete(worksheetDetailIds)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Now delete the order inventories
|
|
681
|
+
await tx.getRepository(OrderInventory).delete(orderInventoriesToDelete)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Update order product totals
|
|
686
|
+
const remainingOrderInventories = (await tx.getRepository(OrderInventory).find({
|
|
687
|
+
where: { orderProduct }
|
|
688
|
+
})) as OrderInventory[]
|
|
689
|
+
|
|
690
|
+
const totalReleaseQty = remainingOrderInventories.reduce((sum, oi) => sum + (oi.releaseQty || 0), 0)
|
|
691
|
+
const totalReleaseUomValue = remainingOrderInventories.reduce((sum, oi) => sum + (oi.releaseUomValue || 0), 0)
|
|
692
|
+
|
|
693
|
+
await tx.getRepository(OrderProduct).update(orderProduct.id, {
|
|
694
|
+
releaseQty: totalReleaseQty,
|
|
695
|
+
releaseUomValue: totalReleaseUomValue,
|
|
696
|
+
updater: user
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function assignAdditionalInventory(
|
|
701
|
+
tx: EntityManager,
|
|
702
|
+
orderProduct: OrderProduct,
|
|
703
|
+
qtyDiff: number,
|
|
704
|
+
uomValueDiff: number,
|
|
705
|
+
user: User,
|
|
706
|
+
releaseGood: ReleaseGood,
|
|
707
|
+
domain: Domain
|
|
708
|
+
) {
|
|
709
|
+
// Get customer bizplace from releaseGood (it's already loaded)
|
|
710
|
+
// If not loaded, fetch it using bizplaceId
|
|
711
|
+
let customerBizplace = releaseGood.bizplace as Bizplace | undefined
|
|
712
|
+
if (!customerBizplace && releaseGood.bizplaceId) {
|
|
713
|
+
customerBizplace = await tx.getRepository(Bizplace).findOne({
|
|
714
|
+
where: { id: releaseGood.bizplaceId }
|
|
715
|
+
})
|
|
716
|
+
}
|
|
717
|
+
if (!customerBizplace) {
|
|
718
|
+
throw new ApiError('E04', 'Customer bizplace not found for release order')
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Get release shelf life override from contact point (deliverTo)
|
|
722
|
+
let releaseShelfLifeOverride: number | null = null
|
|
723
|
+
if (releaseGood.deliverToId) {
|
|
724
|
+
const deliverToContactPoint: ContactPoint | null = await tx
|
|
725
|
+
.getRepository(ContactPoint)
|
|
726
|
+
.findOne({ where: { id: releaseGood.deliverToId } })
|
|
727
|
+
if (deliverToContactPoint?.releaseShelfLife != null && deliverToContactPoint.releaseShelfLife !== 0) {
|
|
728
|
+
releaseShelfLifeOverride = deliverToContactPoint.releaseShelfLife
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Get picking product setting
|
|
733
|
+
const pickingProductSetting: Setting = await tx.getRepository(Setting).findOne({
|
|
734
|
+
where: { domain, name: 'rule-for-picking-product' }
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
// Determine orderInventory status based on releaseGood status
|
|
738
|
+
let orderInventoryStatus: string
|
|
739
|
+
switch (releaseGood.status) {
|
|
740
|
+
case ORDER_STATUS.PENDING:
|
|
741
|
+
orderInventoryStatus = ORDER_INVENTORY_STATUS.PENDING
|
|
742
|
+
break
|
|
743
|
+
case ORDER_STATUS.PENDING_RECEIVE:
|
|
744
|
+
orderInventoryStatus = ORDER_INVENTORY_STATUS.PENDING_RECEIVE
|
|
745
|
+
break
|
|
746
|
+
case ORDER_STATUS.PENDING_WORKSHEET:
|
|
747
|
+
orderInventoryStatus = ORDER_INVENTORY_STATUS.PENDING_WORKSHEET
|
|
748
|
+
break
|
|
749
|
+
case ORDER_STATUS.READY_TO_PICK:
|
|
750
|
+
orderInventoryStatus = ORDER_INVENTORY_STATUS.READY_TO_PICK
|
|
751
|
+
break
|
|
752
|
+
default:
|
|
753
|
+
// Default to PENDING_WORKSHEET for other statuses
|
|
754
|
+
orderInventoryStatus = ORDER_INVENTORY_STATUS.PENDING_WORKSHEET
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Ensure product is loaded
|
|
758
|
+
if (!orderProduct.product) {
|
|
759
|
+
orderProduct.product = await tx.getRepository(Product).findOne({
|
|
760
|
+
where: { id: orderProduct.productId }
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const product = orderProduct.product
|
|
765
|
+
if (!product) {
|
|
766
|
+
throw new ApiError('E04', 'Product not found for order product')
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Determine picking strategy sortings
|
|
770
|
+
let sortings: any = []
|
|
771
|
+
switch (product.pickingStrategy) {
|
|
772
|
+
case 'LIFO':
|
|
773
|
+
sortings.push({ name: 'iv.created_at', desc: true })
|
|
774
|
+
if (pickingProductSetting?.value) {
|
|
775
|
+
for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
|
|
776
|
+
sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
sortings.push({ name: 'loc.name', desc: false }, { name: 'iv.created_at', desc: false })
|
|
780
|
+
}
|
|
781
|
+
break
|
|
782
|
+
case 'FEFO':
|
|
783
|
+
sortings.push({ name: 'iv.expiration_date', desc: false }, { name: 'iv.created_at', desc: false })
|
|
784
|
+
if (pickingProductSetting?.value) {
|
|
785
|
+
for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
|
|
786
|
+
sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
sortings.push({ name: 'loc.name', desc: false }, { name: 'iv.created_at', desc: false })
|
|
790
|
+
}
|
|
791
|
+
break
|
|
792
|
+
case 'FMFO':
|
|
793
|
+
sortings.push({ name: 'iv.manufacture_date', desc: false }, { name: 'iv.created_at', desc: false })
|
|
794
|
+
if (pickingProductSetting?.value) {
|
|
795
|
+
for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
|
|
796
|
+
sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
|
|
797
|
+
}
|
|
798
|
+
} else {
|
|
799
|
+
sortings.push({ name: 'loc.name', desc: false }, { name: 'iv.created_at', desc: false })
|
|
800
|
+
}
|
|
801
|
+
break
|
|
802
|
+
case 'LOCATION':
|
|
803
|
+
if (pickingProductSetting?.value) {
|
|
804
|
+
for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
|
|
805
|
+
sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
sortings.push({ name: 'loc.name', desc: false }, { name: 'iv.created_at', desc: false })
|
|
809
|
+
}
|
|
810
|
+
break
|
|
811
|
+
// Every other case includes 'FIFO' will be applicable for this case
|
|
812
|
+
default:
|
|
813
|
+
sortings.push({ name: 'iv.created_at', desc: false })
|
|
814
|
+
if (pickingProductSetting?.value) {
|
|
815
|
+
for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
|
|
816
|
+
sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
sortings.push({ name: 'loc.name', desc: false })
|
|
820
|
+
sortings.push({ name: 'iv.pallet_id', desc: false })
|
|
821
|
+
}
|
|
822
|
+
break
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
let queryFilters: any[] = []
|
|
826
|
+
let queryStrings = queryFilters.reduce(
|
|
827
|
+
(acc, itm, idx, arr) => {
|
|
828
|
+
acc.values.push(itm.filters)
|
|
829
|
+
|
|
830
|
+
switch (itm?.operator) {
|
|
831
|
+
case 'notin':
|
|
832
|
+
case 'in':
|
|
833
|
+
acc.query.push(
|
|
834
|
+
`${itm.query} ${itm.operator == 'notin' ? 'NOT IN' : 'IN'} (${itm.filters
|
|
835
|
+
.map((itm, idx) => {
|
|
836
|
+
return `$${acc.values.length + 1}`
|
|
837
|
+
})
|
|
838
|
+
.join(',')})`
|
|
839
|
+
)
|
|
840
|
+
break
|
|
841
|
+
|
|
842
|
+
default:
|
|
843
|
+
acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
|
|
844
|
+
break
|
|
845
|
+
}
|
|
846
|
+
acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
|
|
847
|
+
return acc
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
query: [],
|
|
851
|
+
values: []
|
|
852
|
+
}
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
let sortQuery = sortings
|
|
856
|
+
.map(itm => {
|
|
857
|
+
return `${itm.name} ${itm.desc ? 'DESC' : 'ASC'}`
|
|
858
|
+
})
|
|
859
|
+
.join(`,`)
|
|
860
|
+
|
|
861
|
+
// Ensure productDetail is loaded for uom
|
|
862
|
+
if (!orderProduct.productDetail) {
|
|
863
|
+
orderProduct.productDetail = await tx.getRepository(ProductDetail).findOne({
|
|
864
|
+
where: { id: orderProduct.productDetailId }
|
|
865
|
+
})
|
|
866
|
+
}
|
|
867
|
+
const productDetail = orderProduct.productDetail as ProductDetail | null
|
|
868
|
+
const uom = (productDetail && (productDetail as any).uom) || product.uom || 'UN'
|
|
869
|
+
|
|
870
|
+
let params = [
|
|
871
|
+
user.id,
|
|
872
|
+
domain.id,
|
|
873
|
+
customerBizplace.id,
|
|
874
|
+
orderProduct.packingType,
|
|
875
|
+
orderProduct.packingSize,
|
|
876
|
+
product.id,
|
|
877
|
+
INVENTORY_STATUS.STORED,
|
|
878
|
+
LOCATION_TYPE.QUARANTINE,
|
|
879
|
+
LOCATION_TYPE.RESERVE,
|
|
880
|
+
LOCATION_TYPE.STORAGE
|
|
881
|
+
]
|
|
882
|
+
|
|
883
|
+
let batchId = orderProduct.batchId && orderProduct.batchId !== '-' ? String(orderProduct.batchId).trim() : null
|
|
884
|
+
if (batchId) params.push(batchId)
|
|
885
|
+
params = [...params, ...queryStrings.values]
|
|
886
|
+
|
|
887
|
+
// Handle warehouse code filtering
|
|
888
|
+
const warehouseCode = (orderProduct as any).warehouseCode
|
|
889
|
+
const warehouseNameFilter = warehouseCode ? `AND w.name = $${params.length + 1}` : ''
|
|
890
|
+
if (warehouseCode) {
|
|
891
|
+
params.push(String(warehouseCode).trim())
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Add release shelf life override parameter
|
|
895
|
+
const releaseShelfLifeParamIndex = params.length + 1
|
|
896
|
+
params.push(releaseShelfLifeOverride)
|
|
897
|
+
|
|
898
|
+
// Build the sophisticated SQL query with window functions
|
|
899
|
+
let query = `
|
|
900
|
+
update inventories tgt set locked_qty = coalesce(locked_qty,0) + src.reserve_qty,
|
|
901
|
+
locked_uom_value = coalesce(locked_uom_value,0) + src.reserve_uom_value,
|
|
902
|
+
updated_at = NOW(),
|
|
903
|
+
updater_id = $1
|
|
904
|
+
from (
|
|
905
|
+
select "id", "pallet_id","carton_id", "batch_id", "batch_id_ref", "unit", "uom", "packing_type", "packing_size", "manufacture_year",
|
|
906
|
+
"reserve_qty", (("uom_value"/"qty") * "reserve_qty") as "reserve_uom_value" from (
|
|
907
|
+
select "sort_seq", "id", "pallet_id", "batch_id", "batch_id_ref", "unit", "uom", "packing_type", "packing_size", "manufacture_year", "carton_id", "uom_value", "locked_uom_value", "qty", "locked_qty", "created_at",
|
|
908
|
+
"release_qty", "release_uom_value", lock_amount,
|
|
909
|
+
case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end as "reserve_qty"
|
|
910
|
+
from (
|
|
911
|
+
select *,
|
|
912
|
+
(
|
|
913
|
+
case when (qty - greatest(locked_qty, 0) - greatest(unassigned_qty, 0)) < 0 then 0
|
|
914
|
+
else qty - greatest(locked_qty, 0) - greatest(unassigned_qty, 0) end
|
|
915
|
+
) as available_qty,
|
|
916
|
+
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
|
+
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,
|
|
922
|
+
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
|
|
924
|
+
UNION
|
|
925
|
+
(
|
|
926
|
+
SELECT ROW_NUMBER() OVER(PARTITION BY iv.domain_id ORDER BY wiar.rank ${
|
|
927
|
+
sortQuery ? ', ' + sortQuery : ''
|
|
928
|
+
}) as sort_seq,
|
|
929
|
+
iv.id, iv.pallet_id, iv.batch_id, iv.batch_id_ref,
|
|
930
|
+
iv.unit, iv.uom, iv.packing_type, iv.packing_size,
|
|
931
|
+
iv.manufacture_year, iv.carton_id, iv.uom_value,
|
|
932
|
+
coalesce(iv.locked_uom_value,0) as locked_uom_value, iv.qty, greatest(coalesce(iv.locked_qty,0),0) as locked_qty, coalesce(pds.unassigned_uom_value,0) as unassigned_uom_value, greatest(coalesce(pds.unassigned_qty,0),0) as unassigned_qty,
|
|
933
|
+
iv.created_at, 0 as release_qty, 0 as release_uom_value
|
|
934
|
+
FROM "inventories" "iv"
|
|
935
|
+
LEFT JOIN "product_detail_stocks" "pds" ON "pds"."product_detail_id" = "iv"."product_detail_id"
|
|
936
|
+
INNER JOIN "locations" "loc" ON "loc"."id"="iv"."location_id"
|
|
937
|
+
INNER JOIN "warehouses" "w" ON "w"."id" = "loc"."warehouse_id"
|
|
938
|
+
INNER JOIN "products" "p" ON "p"."id"="iv"."product_id"
|
|
939
|
+
INNER JOIN "warehouse_inventory_assignment_rankings" "wiar" ON "wiar"."location_type"="loc"."type"
|
|
940
|
+
WHERE case when coalesce("iv"."locked_qty",0) < 0 then 0 else ("iv"."qty" - coalesce("iv"."locked_qty",0)) end > 0 AND
|
|
941
|
+
"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, $10)
|
|
942
|
+
AND ${batchId ? `"iv"."batch_id" = $11` : `1=1`}
|
|
943
|
+
${warehouseNameFilter}
|
|
944
|
+
AND "iv"."obsolete" = false AND (
|
|
945
|
+
"iv"."expiration_date" IS NULL
|
|
946
|
+
OR
|
|
947
|
+
CASE
|
|
948
|
+
WHEN $${releaseShelfLifeParamIndex}::integer IS NOT NULL AND $${releaseShelfLifeParamIndex}::integer > 0 THEN
|
|
949
|
+
CURRENT_DATE < ("iv"."expiration_date" - $${releaseShelfLifeParamIndex}::integer)
|
|
950
|
+
WHEN "p"."min_outbound_shelf_life" IS NOT NULL AND "p"."min_outbound_shelf_life" > 0 THEN
|
|
951
|
+
CURRENT_DATE < ("iv"."expiration_date" - "p"."min_outbound_shelf_life")
|
|
952
|
+
ELSE
|
|
953
|
+
TRUE
|
|
954
|
+
END
|
|
955
|
+
)
|
|
956
|
+
${queryStrings.query.length > 0 ? `AND ${queryStrings.query.join(' AND ')}` : ''}
|
|
957
|
+
ORDER BY wiar.rank ${sortQuery ? ', ' + sortQuery : ''}
|
|
958
|
+
)
|
|
959
|
+
) dt1
|
|
960
|
+
) dt2 where case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end > 0
|
|
961
|
+
) 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",
|
|
966
|
+
tgt."locked_qty", tgt."uom_value", tgt."locked_uom_value", src."reserve_qty", src."reserve_uom_value";`
|
|
967
|
+
|
|
968
|
+
let updatedInventories = await tx.getRepository(Inventory).query(query, params)
|
|
969
|
+
|
|
970
|
+
let totalAssigned =
|
|
971
|
+
updatedInventories[0]?.reduce((acc: number, inv: any) => {
|
|
972
|
+
return acc + inv.reserve_qty
|
|
973
|
+
}, 0) || 0
|
|
974
|
+
|
|
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
|
+
}
|
|
985
|
+
|
|
986
|
+
if (Math.abs(roundedQtyDiff - totalAssigned) > 0.001) {
|
|
987
|
+
// Using small epsilon for float comparison
|
|
988
|
+
throw new ApiError('E01', 'INSUFFICIENT_STOCK')
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Create order inventory records for each assigned inventory
|
|
992
|
+
if (updatedInventories[0] && updatedInventories[0].length > 0) {
|
|
993
|
+
// Ensure releaseGood.id is available (required for TypeORM relation)
|
|
994
|
+
if (!releaseGood.id) {
|
|
995
|
+
throw new ApiError('E04', 'Release good ID is required for order inventory')
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Ensure orderProduct.id is available (required for TypeORM relation)
|
|
999
|
+
if (!orderProduct.id) {
|
|
1000
|
+
throw new ApiError('E04', 'Order product ID is required for order inventory')
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Get existing order inventories for this order product to check for duplicates
|
|
1004
|
+
const existingOrderInventories: OrderInventory[] = await tx.getRepository(OrderInventory).find({
|
|
1005
|
+
where: { orderProduct },
|
|
1006
|
+
relations: ['inventory']
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
// Check if worksheets exist for this release good
|
|
1010
|
+
const existingWorksheets: Worksheet[] = await tx.getRepository(Worksheet).find({
|
|
1011
|
+
where: {
|
|
1012
|
+
releaseGood: { id: releaseGood.id },
|
|
1013
|
+
type: WORKSHEET_TYPE.PICKING,
|
|
1014
|
+
status: In([WORKSHEET_STATUS.DEACTIVATED])
|
|
1015
|
+
},
|
|
1016
|
+
relations: ['bizplace']
|
|
1017
|
+
})
|
|
1018
|
+
|
|
1019
|
+
const newOrderInventories: OrderInventory[] = []
|
|
1020
|
+
|
|
1021
|
+
for (const inv of updatedInventories[0]) {
|
|
1022
|
+
// Check if an order inventory already exists for this inventory ID
|
|
1023
|
+
const existingOI = existingOrderInventories.find(
|
|
1024
|
+
(oi: OrderInventory) => oi.inventory && (oi.inventory as Inventory).id === inv.id
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
let savedOrderInventory: OrderInventory
|
|
1028
|
+
|
|
1029
|
+
if (existingOI) {
|
|
1030
|
+
// Update existing order inventory
|
|
1031
|
+
const updatedReleaseQty = (existingOI.releaseQty || 0) + inv.reserve_qty
|
|
1032
|
+
const updatedReleaseUomValue = (existingOI.releaseUomValue || 0) + inv.reserve_uom_value
|
|
1033
|
+
|
|
1034
|
+
await tx.getRepository(OrderInventory).update(existingOI.id, {
|
|
1035
|
+
releaseQty: updatedReleaseQty,
|
|
1036
|
+
releaseUomValue: updatedReleaseUomValue,
|
|
1037
|
+
updater: user
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
// Reload to get updated entity
|
|
1041
|
+
savedOrderInventory = (await tx.getRepository(OrderInventory).findOne({
|
|
1042
|
+
where: { id: existingOI.id },
|
|
1043
|
+
relations: ['inventory']
|
|
1044
|
+
})) as OrderInventory
|
|
1045
|
+
} else {
|
|
1046
|
+
// Create new order inventory
|
|
1047
|
+
savedOrderInventory = await tx.getRepository(OrderInventory).save(
|
|
1048
|
+
Object.assign(new OrderInventory(), {
|
|
1049
|
+
name: uuidv4(),
|
|
1050
|
+
domain: domain, // Use passed domain parameter
|
|
1051
|
+
bizplace: customerBizplace, // Use customerBizplace from releaseGood
|
|
1052
|
+
releaseGood: releaseGood,
|
|
1053
|
+
releaseGoodId: releaseGood.id, // Explicitly set to ensure foreign key is saved
|
|
1054
|
+
orderProduct: orderProduct,
|
|
1055
|
+
orderProductId: orderProduct.id, // Explicitly set to ensure foreign key is saved
|
|
1056
|
+
product: product,
|
|
1057
|
+
productDetail: productDetail,
|
|
1058
|
+
inventory: { id: inv.id } as Inventory,
|
|
1059
|
+
packingType: inv.packing_type,
|
|
1060
|
+
packingSize: inv.packing_size,
|
|
1061
|
+
batchId: inv.batch_id,
|
|
1062
|
+
releaseQty: inv.reserve_qty,
|
|
1063
|
+
releaseUomValue: inv.reserve_uom_value,
|
|
1064
|
+
uom: inv.uom,
|
|
1065
|
+
type: ORDER_TYPES.RELEASE_OF_GOODS,
|
|
1066
|
+
status: orderInventoryStatus,
|
|
1067
|
+
creator: user,
|
|
1068
|
+
updater: user
|
|
1069
|
+
})
|
|
1070
|
+
)
|
|
1071
|
+
newOrderInventories.push(savedOrderInventory)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Create worksheet details for newly created order inventories if worksheets exist
|
|
1076
|
+
if (existingWorksheets.length > 0 && newOrderInventories.length > 0) {
|
|
1077
|
+
for (const worksheet of existingWorksheets) {
|
|
1078
|
+
const worksheetDetails = newOrderInventories.map((oi: OrderInventory) => ({
|
|
1079
|
+
domain: domain,
|
|
1080
|
+
bizplace: (worksheet as Worksheet).bizplace || customerBizplace,
|
|
1081
|
+
worksheet: worksheet,
|
|
1082
|
+
name: WorksheetNoGenerator.pickingDetail(),
|
|
1083
|
+
type: WORKSHEET_TYPE.PICKING,
|
|
1084
|
+
status: WORKSHEET_STATUS.DEACTIVATED,
|
|
1085
|
+
targetInventory: oi,
|
|
1086
|
+
creator: user,
|
|
1087
|
+
updater: user
|
|
1088
|
+
}))
|
|
1089
|
+
|
|
1090
|
+
await tx.getRepository(WorksheetDetail).save(worksheetDetails)
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function addNewOrderProduct(
|
|
1097
|
+
tx: EntityManager,
|
|
1098
|
+
orderProductReq: any,
|
|
1099
|
+
releaseGood: ReleaseGood,
|
|
1100
|
+
domain: Domain,
|
|
1101
|
+
user: User,
|
|
1102
|
+
context: any,
|
|
1103
|
+
hasOrderInventories: boolean
|
|
1104
|
+
) {
|
|
1105
|
+
const sku = orderProductReq.product?.sku || orderProductReq.sku
|
|
1106
|
+
const refCode = orderProductReq.product?.refCode || orderProductReq.refCode
|
|
1107
|
+
const packingType = orderProductReq.packingType
|
|
1108
|
+
const packingSize = orderProductReq.packingSize
|
|
1109
|
+
const releaseQty = orderProductReq.releaseQty !== undefined ? orderProductReq.releaseQty : orderProductReq.qty
|
|
1110
|
+
const refItemId = orderProductReq.product?.refItemId || orderProductReq.refItemId || null
|
|
1111
|
+
|
|
1112
|
+
if (!sku && !refCode) {
|
|
1113
|
+
throw new ApiError('E01', 'sku or refCode is required for new items')
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (!releaseQty || releaseQty <= 0) {
|
|
1117
|
+
throw new ApiError('E01', 'releaseQty must be greater than 0')
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Ensure releaseGood has an ID (required for TypeORM relation)
|
|
1121
|
+
if (!releaseGood.id) {
|
|
1122
|
+
throw new ApiError('E04', 'Release good ID is required')
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Get company bizplace for product lookup
|
|
1126
|
+
const customerCompanyBizplace: Bizplace = await getCompanyBizplace(null, null, releaseGood.bizplaceId, tx)
|
|
1127
|
+
|
|
1128
|
+
// Find product detail
|
|
1129
|
+
const qb: SelectQueryBuilder<ProductDetail> = tx
|
|
1130
|
+
.getRepository(ProductDetail)
|
|
1131
|
+
.createQueryBuilder('pd')
|
|
1132
|
+
.innerJoinAndSelect('pd.product', 'prod')
|
|
1133
|
+
.where('prod.bizplace_id = :bizplaceId', { bizplaceId: customerCompanyBizplace.id })
|
|
1134
|
+
.andWhere('pd.deleted_at IS NULL')
|
|
1135
|
+
|
|
1136
|
+
if (refCode) {
|
|
1137
|
+
qb.andWhere('pd.ref_code = :refCode', { refCode })
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (sku) {
|
|
1141
|
+
qb.andWhere('prod.sku = :sku', { sku })
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (packingType) {
|
|
1145
|
+
qb.andWhere('pd.packing_type = :packingType', { packingType })
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (packingSize) {
|
|
1149
|
+
qb.andWhere('pd.packing_size = :packingSize', { packingSize })
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (!refCode && !packingType && !packingSize) {
|
|
1153
|
+
qb.andWhere('pd.is_default = :isDefault', { isDefault: true })
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const productDetail: ProductDetail = await qb.getOne()
|
|
1157
|
+
|
|
1158
|
+
if (!productDetail) {
|
|
1159
|
+
throw new ApiError('E04', `Product not found: ${sku || refCode}`)
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Validate decimal places
|
|
1163
|
+
if (!productDetail.product.isInventoryDecimal && releaseQty % 1 !== 0) {
|
|
1164
|
+
throw new ApiError('E01', `releaseQty must be an integer for product ${sku || refCode}`)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Round to 3 decimal places
|
|
1168
|
+
const roundedReleaseQty = productDetail.product.isInventoryDecimal
|
|
1169
|
+
? Math.round(releaseQty * 1000) / 1000
|
|
1170
|
+
: Math.round(releaseQty)
|
|
1171
|
+
const releaseUomValue = roundedReleaseQty * (productDetail.uomValue || 1)
|
|
1172
|
+
|
|
1173
|
+
// Ensure bizplace is loaded - fetch it if not already loaded
|
|
1174
|
+
let bizplace = releaseGood.bizplace
|
|
1175
|
+
if (!bizplace && releaseGood.bizplaceId) {
|
|
1176
|
+
bizplace = await tx.getRepository(Bizplace).findOne({ where: { id: releaseGood.bizplaceId } })
|
|
1177
|
+
if (!bizplace) {
|
|
1178
|
+
throw new ApiError('E04', 'Bizplace not found for release order')
|
|
1179
|
+
}
|
|
1180
|
+
} else if (!bizplace) {
|
|
1181
|
+
throw new ApiError('E04', 'Bizplace is required for order product')
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const repo = tx.getRepository(OrderProduct)
|
|
1185
|
+
|
|
1186
|
+
const created = repo.create({
|
|
1187
|
+
name: `OP-` + uuidv4(),
|
|
1188
|
+
|
|
1189
|
+
// Use relation stubs (IDs only) to force FK persistence
|
|
1190
|
+
domain: { id: domain.id } as any,
|
|
1191
|
+
bizplace: { id: (bizplace as any).id } as any,
|
|
1192
|
+
releaseGood: { id: releaseGood.id } as any,
|
|
1193
|
+
releaseGoodId: releaseGood.id, // Explicitly set the column value
|
|
1194
|
+
product: { id: (productDetail.product as any).id } as any,
|
|
1195
|
+
productDetail: { id: (productDetail as any).id } as any,
|
|
1196
|
+
packingType: productDetail.packingType,
|
|
1197
|
+
packingSize: packingSize || productDetail.packingSize,
|
|
1198
|
+
batchId: orderProductReq.batchId || '',
|
|
1199
|
+
refItemId: refItemId,
|
|
1200
|
+
releaseQty: roundedReleaseQty,
|
|
1201
|
+
releaseUomValue: releaseUomValue,
|
|
1202
|
+
uom: productDetail.uom,
|
|
1203
|
+
uomValue: productDetail.uomValue,
|
|
1204
|
+
packQty: 0,
|
|
1205
|
+
actualPackQty: 0,
|
|
1206
|
+
palletQty: 0,
|
|
1207
|
+
actualPalletQty: 0,
|
|
1208
|
+
type: ORDER_TYPES.RELEASE_OF_GOODS,
|
|
1209
|
+
status: ORDER_PRODUCT_STATUS.ASSIGNED,
|
|
1210
|
+
unitPrice: orderProductReq.unitPrice || null,
|
|
1211
|
+
remark: orderProductReq.remark || null,
|
|
1212
|
+
creator: user,
|
|
1213
|
+
updater: user
|
|
107
1214
|
})
|
|
108
1215
|
|
|
109
|
-
|
|
1216
|
+
const newOrderProduct = await repo.save(created)
|
|
1217
|
+
|
|
1218
|
+
if (hasOrderInventories) {
|
|
1219
|
+
// Assign inventory - pass releaseGood so status can be determined
|
|
1220
|
+
await assignAdditionalInventory(tx, newOrderProduct, roundedReleaseQty, releaseUomValue, user, releaseGood, domain)
|
|
1221
|
+
} else {
|
|
1222
|
+
// Update ProductDetailStock
|
|
1223
|
+
await tx
|
|
1224
|
+
.getRepository(ProductDetailStock)
|
|
1225
|
+
.createQueryBuilder()
|
|
1226
|
+
.update(ProductDetailStock)
|
|
1227
|
+
.set({
|
|
1228
|
+
unassignedQty: () => `"unassigned_qty" + ${roundedReleaseQty}`,
|
|
1229
|
+
unassignedUomValue: () => `"unassigned_uom_value" + ${releaseUomValue}`
|
|
1230
|
+
})
|
|
1231
|
+
.where({ productDetail: productDetail.id })
|
|
1232
|
+
.execute()
|
|
1233
|
+
}
|
|
110
1234
|
}
|