@things-factory/operato-hub 4.3.28 → 4.3.31

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.
@@ -1,15 +1,36 @@
1
- import gql from 'graphql-tag'
2
- import { EntityManager, getConnection, SelectQueryBuilder } from 'typeorm'
1
+ import { EntityManager, getConnection, getRepository, In, Not, Repository, SelectQueryBuilder } from 'typeorm'
2
+ import { v4 as uuidv4 } from 'uuid'
3
3
 
4
4
  import { restfulApiRouter as router } from '@things-factory/api'
5
+ import { User } from '@things-factory/auth-base'
5
6
  import { Bizplace, getCompanyBizplace } from '@things-factory/biz-base'
7
+ import { logger } from '@things-factory/env'
8
+ import { generateId } from '@things-factory/id-rule-base'
6
9
  import { createPayloadLog, PayloadType } from '@things-factory/integration-base'
7
- import { Product, ProductBundle, ProductDetail } from '@things-factory/product-base'
8
- import { ORDER_TYPES, OrderInventory } from '@things-factory/sales-base'
10
+ import { Sellercraft } from '@things-factory/integration-sellercraft'
11
+ import { MarketplaceOrder } from '@things-factory/marketplace-base'
12
+ import { ProductBundle, ProductDetail } from '@things-factory/product-base'
13
+ import {
14
+ createDraftReleaseGoodFunction,
15
+ DRAFT_RELEASE_ORDER_STATUS,
16
+ DraftReleaseGood,
17
+ ORDER_INVENTORY_STATUS,
18
+ ORDER_NUMBER_RULE_TYPE,
19
+ ORDER_NUMBER_SETTING_KEY,
20
+ ORDER_PRODUCT_STATUS,
21
+ ORDER_STATUS,
22
+ ORDER_TYPES,
23
+ OrderInventory,
24
+ OrderNoGenerator,
25
+ OrderProduct,
26
+ ReleaseGood,
27
+ SellercraftController
28
+ } from '@things-factory/sales-base'
9
29
  import { Setting } from '@things-factory/setting-base'
30
+ import { Domain } from '@things-factory/shell'
10
31
  import { Inventory, INVENTORY_STATUS, LOCATION_TYPE } from '@things-factory/warehouse-base'
11
- import { WorksheetDetail } from '@things-factory/worksheet-base'
12
32
 
33
+ import { ValidationError } from '../../../../errors/index'
13
34
  import { businessMiddleware } from '../../../../middlewares/business-middleware'
14
35
  import { warehouseMiddleware } from '../../../../middlewares/warehouse-middleware'
15
36
 
@@ -22,10 +43,15 @@ router.post(
22
43
  warehouseMiddleware,
23
44
  async (context, next) => {
24
45
  return await getConnection().transaction(async (tx: EntityManager) => {
25
- const { client, domain } = context.state
46
+ const { client, domain, user } = context.state
26
47
  const { warehouseId: customerBizplaceId } = context.params
27
48
  const { t } = context
28
49
  const bodyReq = context.request.body
50
+ const requiredDraft: Boolean = bodyReq.hasOwnProperty('requiredDraft') ? bodyReq.requiredDraft : true
51
+ let releaseGood
52
+ let massagedData = { orderProducts: [], combinedItems: [] }
53
+ // const isAutoAssign: Boolean = bodyReq.hasOwnProperty('isAutoAssign') ? bodyReq.isAutoAssign : true
54
+
29
55
  try {
30
56
  if (!customerBizplaceId) context.throw(404, t('error.warehouse id not found'))
31
57
 
@@ -35,691 +61,178 @@ router.post(
35
61
  debug('post:/add-release-order request.body', bodyReq)
36
62
  await checkMandatoryKey(context, bodyReq)
37
63
 
38
- const isAutoAssign: Boolean = bodyReq.hasOwnProperty('isAutoAssign') ? bodyReq.isAutoAssign : true
39
- const requiredDraft: Boolean = bodyReq.hasOwnProperty('requiredDraft') ? bodyReq.requiredDraft : true
40
-
41
64
  // get customer company's bizplace
42
65
  const custCompanyBizplace: Bizplace = await getCompanyBizplace(null, null, customerBizplaceId, tx)
43
66
  const customerBizplace: Bizplace = await tx.getRepository(Bizplace).findOne({
44
- where: { id: customerBizplaceId }
67
+ where: { id: customerBizplaceId },
68
+ relations: ['domain']
45
69
  })
46
70
 
47
- let locationSortingRules = []
48
- const inventoryAssignmentSetting: Setting = await tx.getRepository(Setting).findOne({
49
- where: { domain, name: 'rule-for-inventory-assignment' }
50
- })
51
-
52
- if (inventoryAssignmentSetting) {
53
- let locationSetting = JSON.parse(inventoryAssignmentSetting.value)
54
- for (const key in locationSetting) {
55
- locationSortingRules.push({ name: key, desc: locationSetting[key] == 'ASC' ? false : true })
56
- }
57
- } else {
58
- const pickingProductSetting: Setting = await tx.getRepository(Setting).findOne({
59
- where: { domain, name: 'rule-for-picking-product' }
60
- })
61
- if (pickingProductSetting) {
62
- let locationSetting = JSON.parse(pickingProductSetting.value)
63
- for (const key in locationSetting) {
64
- locationSortingRules.push({ name: key, desc: locationSetting[key] == 'ASC' ? false : true })
65
- }
66
- }
71
+ releaseGood = {
72
+ domain,
73
+ bizplace: customerBizplace,
74
+ companyBizplace: custCompanyBizplace,
75
+ courierOption: bodyReq.courierOption,
76
+ collectionOrderNo: bodyReq.collectionOrderNo,
77
+ exportOption: bodyReq.exportOption,
78
+ ownTransport: bodyReq.ownTransport,
79
+ packingOption: bodyReq.packingOption,
80
+ refNo: bodyReq.refNo,
81
+ refNo2: bodyReq?.refNo2,
82
+ refNo3: bodyReq?.refNo3,
83
+ releaseDate: bodyReq.releaseDate,
84
+ type: bodyReq.type,
85
+ marketplaceOrderStatus: bodyReq?.marketplaceOrderStatus,
86
+ remark: bodyReq?.remark || null,
87
+ billingAddress: bodyReq?.billTo?.billingAddress || null,
88
+ deliveryAddress1: bodyReq?.deliverTo?.deliveryAddress1 || null,
89
+ deliveryAddress2: bodyReq?.deliverTo?.deliveryAddress2 || null,
90
+ deliveryAddress3: bodyReq?.deliverTo?.deliveryAddress3 || null,
91
+ deliveryAddress4: bodyReq?.deliverTo?.deliveryAddress4 || null,
92
+ deliveryAddress5: bodyReq?.deliverTo?.deliveryAddress5 || null,
93
+ attentionTo: bodyReq?.deliverTo?.attentionTo || null,
94
+ attentionCompany: bodyReq?.deliverTo?.attentionCompany || null,
95
+ city: bodyReq?.deliverTo?.city || null,
96
+ state: bodyReq?.deliverTo?.state || null,
97
+ postalCode: bodyReq?.deliverTo?.postalCode || null,
98
+ country: bodyReq?.deliverTo?.country || null,
99
+ phone1: bodyReq?.deliverTo?.phone1 || null,
100
+ phone2: bodyReq?.deliverTo?.phone2 || null,
101
+ email: bodyReq?.deliverTo?.email || null,
102
+ transporter: bodyReq?.transporter,
103
+ trackingNo: bodyReq?.trackingNo,
104
+ airwayBill: bodyReq?.airwayBill,
105
+ invoice: bodyReq?.invoice,
106
+ storeName: bodyReq?.storeName,
107
+ storeId: bodyReq?.storeId,
108
+ routeId: bodyReq?.routeId,
109
+ stopId: bodyReq?.stopId
67
110
  }
68
111
 
69
- const orderInventories: OrderInventory[] = bodyReq.orderInventories
70
- let newOrderInventories: OrderInventory[] = []
71
- let bundleInfo: any[] = []
72
-
73
- await Promise.all(
74
- orderInventories.map(async orderInventory => {
75
- const sku: string = orderInventory.product?.sku
76
- const refCode: string = orderInventory.product?.refCode
77
- const packingType: string = orderInventory.packingType
78
-
79
- const qb: SelectQueryBuilder<ProductDetail> = tx
80
- .getRepository(ProductDetail)
81
- .createQueryBuilder('PD')
82
- .innerJoinAndSelect('PD.product', 'PROD')
83
- .where('PROD.bizplace_id = :bizplaceId', { bizplaceId: custCompanyBizplace.id })
84
-
85
- if (!sku && !refCode) {
86
- context.throw(404, t('error.sku or refCode not found'))
87
- }
88
-
89
- if (refCode) {
90
- qb.andWhere('PD.ref_code = :refCode', { refCode })
91
- }
92
-
93
- if (sku) {
94
- qb.andWhere('PROD.sku = :sku', { sku })
95
- }
96
-
97
- if (packingType) {
98
- qb.andWhere('PD.packing_type = :packingType', { packingType })
99
- }
100
-
101
- if (!packingType && !refCode) {
102
- qb.andWhere('PD.is_default = :isDefault', { isDefault: true })
103
- }
104
-
105
- const productDetail: ProductDetail = await qb.getOne()
112
+ // check existing order
113
+ await checkExistingOrder(releaseGood, customerBizplace, context, tx)
106
114
 
107
- if (productDetail) {
108
- let newOrderInventory: OrderInventory = Object.assign({}, orderInventory)
115
+ //massage data
116
+ massagedData = await massageOrderItems(releaseGood, bodyReq.orderInventories, context)
109
117
 
110
- newOrderInventory.product.sku = productDetail.product.sku
111
- newOrderInventory.product = productDetail.product
112
- newOrderInventory.packingType = productDetail.packingType
113
- newOrderInventory.uomValue = productDetail.uomValue
114
- newOrderInventories.push(newOrderInventory)
115
- }
116
-
117
- if (!productDetail) {
118
- const qb: SelectQueryBuilder<ProductDetail> = tx
119
- .getRepository(ProductBundle)
120
- .createQueryBuilder('PB')
121
- .innerJoinAndSelect('PB.productBundleSettings', 'PBS')
122
- .innerJoinAndSelect('PBS.productDetail', 'PBD')
123
- .innerJoinAndSelect('PBD.product', 'PROD')
124
- .where('PB.bizplace_id = :bizplaceId', { bizplaceId: custCompanyBizplace.id })
125
-
126
- if (!sku && !refCode) {
127
- context.throw(404, t('error.sku or refCode not found'))
128
- }
129
-
130
- if (refCode) {
131
- qb.andWhere('PB.ref_code = :refCode', { refCode })
132
- }
133
-
134
- if (sku) {
135
- qb.andWhere('PB.sku = :sku', { sku })
136
- }
137
-
138
- if (packingType) {
139
- qb.andWhere('PB.packing_type = :packingType', { packingType })
140
- }
141
-
142
- const foundProductBundle: ProductBundle = await qb.getOne()
143
-
144
- if (foundProductBundle) {
145
- bundleInfo.push({
146
- productBundleId: foundProductBundle.id,
147
- sku: foundProductBundle.sku,
148
- releaseQty: orderInventory.releaseQty
149
- })
150
- const bundleProductSettings: any[] = foundProductBundle.productBundleSettings
151
- bundleProductSettings.map(bundleProductSetting => {
152
- const productDetailBundle: ProductDetail = bundleProductSetting.productDetail
153
-
154
- let newOrderInventory: OrderInventory = Object.assign({}, orderInventory)
155
- newOrderInventory.product.sku = productDetailBundle.product.sku
156
- newOrderInventory.packingType = productDetailBundle.packingType
157
- newOrderInventory.uomValue = productDetailBundle.uomValue
158
- newOrderInventory.releaseQty = orderInventory.releaseQty * bundleProductSetting.bundleQty
159
- newOrderInventory.product = productDetailBundle.product
160
- newOrderInventories.push(newOrderInventory)
118
+ // validation
119
+ let errorList = []
120
+ await Promise.all(
121
+ massagedData.combinedItems.map(async item => {
122
+ let itemList = await tx.query(
123
+ `
124
+ select wboi.* from warehouse_bizplace_onhand_inventories wboi
125
+ where wboi."domainId"= $1 and
126
+ wboi."bizplaceId" = $2 and
127
+ wboi."groupType" = 'SINGLE' and
128
+ wboi."productId" = $3 and
129
+ wboi."packingType" = $4 and
130
+ wboi."packingSize" = $5 and
131
+ wboi."uom" = $6 and
132
+ wboi."remainQty" >= $7 and
133
+ wboi."remainUomValue" >= $8
134
+ `,
135
+ [
136
+ domain.id,
137
+ customerBizplace.id,
138
+ item.product.id,
139
+ item.packingType,
140
+ item.packingSize,
141
+ item.uom,
142
+ item.releaseQty,
143
+ item.releaseUomValue
144
+ ]
145
+ )
146
+ if (itemList.length <= 0) {
147
+ errorList.push(
148
+ JSON.stringify({
149
+ code: 'insufficient stock',
150
+ sku: item.product.sku,
151
+ packingType: item.packingType,
152
+ packingSize: item.packingSize,
153
+ uom: item.uom,
154
+ qty: item.releaseQty,
155
+ uom_value: item.releaseUomValue
161
156
  })
162
- } else {
163
- if (sku && refCode) {
164
- context.throw(404, t(`error.sku ${sku} and refCode ${refCode} not exist in product / bundle master`))
165
- }
166
-
167
- if (sku) {
168
- context.throw(404, t(`error.sku ${sku} not exist in product / bundle master`))
169
- }
170
-
171
- if (refCode) {
172
- context.throw(404, t(`error.refCode ${refCode} not exist in product / bundle master`))
173
- }
174
- }
157
+ )
175
158
  }
159
+ console.log()
176
160
  })
177
161
  )
178
162
 
179
- let finalOrderInventories: OrderInventory[] = []
180
-
181
- // Case 1. Every order inventories has specified batch id
182
- if (newOrderInventories.every((orderInventory: OrderInventory) => orderInventory.batchId)) {
183
- finalOrderInventories = await Promise.all(
184
- newOrderInventories.map(async (orderInventory: OrderInventory) => {
185
- let filters: any = [
186
- {
187
- name: 'batchId',
188
- operator: 'eq',
189
- value: orderInventory.batchId
190
- },
191
- {
192
- name: 'productName',
193
- operator: 'eq',
194
- value: orderInventory.product.sku
195
- },
196
- {
197
- name: 'bizplace',
198
- operator: 'in',
199
- value: [customerBizplaceId]
200
- }
201
- ]
202
-
203
- let sortings: any = []
204
-
205
- switch (orderInventory.product.pickingStrategy) {
206
- case 'FIFO':
207
- sortings.push({ name: 'created_at', desc: false })
208
- break
209
-
210
- case 'LIFO':
211
- sortings.push({ name: 'created_at', desc: true })
212
- break
213
-
214
- case 'FEFO':
215
- sortings.push({ name: 'expiration_date', desc: false })
216
- break
217
-
218
- case 'FMFO':
219
- sortings.push({ name: 'manufacture_date', desc: false })
220
- break
221
- }
222
-
223
- // 2. get all inventory data according to SKU and batchId
224
- const response = await client.query({
225
- query: gql`
226
- query inventoriesByPallet(
227
- $filters: [Filter]
228
- $sortings: [Sorting]
229
- $locationSortingRules: [Sorting]
230
- ) {
231
- inventoriesByPallet(
232
- filters: $filters
233
- sortings: $sortings
234
- locationSortingRules: $locationSortingRules
235
- ) {
236
- items {
237
- product {
238
- id
239
- name
240
- description
241
- }
242
- batchId
243
- packingType
244
- remainQty
245
- remainUomValue
246
- }
247
- total
248
- }
249
- }
250
- `,
251
- variables: { filters, sortings, locationSortingRules },
252
- context: {
253
- ...context,
254
- state: {
255
- ...context.state,
256
- bizplace: customerBizplace,
257
- domain
258
- }
259
- }
260
- } as any)
261
-
262
- if (!response.errors) {
263
- const items = response.data.inventoriesByPallet.items || []
264
- if (requiredDraft) {
265
- if (!items?.length) {
266
- context.body = {
267
- result: await createDraftOrder(
268
- client,
269
- customerBizplace,
270
- bodyReq,
271
- bundleInfo,
272
- domain,
273
- context,
274
- orderInventories,
275
- tx
276
- )
277
- }
278
- return
279
- }
280
- }
281
-
282
- items.forEach(async item => {
283
- if (requiredDraft) {
284
- if (orderInventory.releaseQty > item.remainQty || orderInventory.releaseQty <= 0) {
285
- context.body = {
286
- result: await createDraftOrder(
287
- client,
288
- customerBizplace,
289
- bodyReq,
290
- bundleInfo,
291
- domain,
292
- context,
293
- orderInventories,
294
- tx
295
- )
296
- }
297
- return
298
- }
299
- }
300
-
301
- // 3. add more information to orderInventory
302
- orderInventory.product.id = item.product.id
303
- orderInventory.packingType = item.packingType
304
- orderInventory.batchId = item.batchId
305
- orderInventory.type = ORDER_TYPES.RELEASE_OF_GOODS
306
- orderInventory.releaseUomValue = orderInventory.releaseQty * orderInventory.uomValue
307
- orderInventory.uom = item.uom
308
- })
309
-
310
- return orderInventory
311
- } else {
312
- if (requiredDraft) {
313
- context.body = {
314
- result: await createDraftOrder(
315
- client,
316
- customerBizplace,
317
- bodyReq,
318
- bundleInfo,
319
- domain,
320
- context,
321
- orderInventories,
322
- tx
323
- )
324
- }
325
- return
326
- }
327
- }
328
- })
329
- )
330
- } else {
331
- // Case 2. Some of order inventories doesn't have specified batch id
332
- // MMS order doesn't have batch ID field.
333
- // To cover this case first result of found inventory will be a target of RO
334
-
335
- // To combine items from different bundles
336
- newOrderInventories = combineItems(newOrderInventories)
337
-
338
- for (let i: number = 0; i < newOrderInventories.length; i++) {
339
- const orderInventory: OrderInventory = newOrderInventories[i]
340
- const { product, packingType }: { product: Product; packingType: string } = orderInventory
341
- const pickingStrategy: string = product.pickingStrategy
342
-
343
- if (isAutoAssign) {
344
- let qb: SelectQueryBuilder<Inventory> = tx.getRepository(Inventory).createQueryBuilder('iv')
345
- qb.leftJoinAndSelect('iv.location', 'loc')
346
- .andWhere('"iv"."domain_id" = :domainId')
347
- .andWhere('"iv"."bizplace_id" = :bizplaceId')
348
- .andWhere('"iv"."packing_type" = :packingType')
349
- .andWhere('"iv"."product_id" = :productId')
350
- .andWhere('"iv"."status" = :status')
351
- .andWhere('"loc"."type" NOT IN (:...locationTypes)')
352
- .setParameters({
353
- domainId: domain.id,
354
- bizplaceId: customerBizplace.id,
355
- packingType: packingType,
356
- productId: product.id,
357
- status: INVENTORY_STATUS.STORED,
358
- locationTypes: [LOCATION_TYPE.QUARANTINE, LOCATION_TYPE.RESERVE]
359
- })
360
-
361
- switch (pickingStrategy) {
362
- case 'FIFO':
363
- qb.orderBy('"iv"."created_at"', 'ASC')
364
- if (locationSortingRules?.length > 0) {
365
- locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
366
- qb.addOrderBy(`loc.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
367
- })
368
- }
369
- break
370
-
371
- case 'LIFO':
372
- qb.orderBy('"iv"."created_at"', 'DESC')
373
- if (locationSortingRules?.length > 0) {
374
- locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
375
- qb.addOrderBy(`loc.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
376
- })
377
- }
378
- break
379
-
380
- case 'FEFO':
381
- qb.orderBy('"iv"."expiration_date"', 'ASC')
382
- if (locationSortingRules?.length > 0) {
383
- locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
384
- qb.addOrderBy(`loc.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
385
- })
386
- }
387
- break
388
-
389
- case 'FMFO':
390
- qb.orderBy('"iv"."manufacture_date"', 'ASC')
391
- if (locationSortingRules?.length > 0) {
392
- locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
393
- qb.addOrderBy(`loc.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
394
- })
395
- }
396
- break
397
-
398
- case 'LOCATION':
399
- if (locationSortingRules?.length > 0) {
400
- locationSortingRules.forEach((rule: { name: string; desc: boolean }, idx: number) => {
401
- idx === 0
402
- ? qb.orderBy(`loc.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
403
- : qb.addOrderBy(`loc.${rule.name}`, rule.desc ? 'DESC' : 'ASC')
404
- })
405
- } else {
406
- qb.orderBy('"loc"."name"', 'DESC')
407
- }
408
- break
409
- }
410
-
411
- let inventories: Inventory[] = await qb.getMany()
412
- if (requiredDraft) {
413
- if (!inventories?.length) {
414
- context.body = {
415
- result: await createDraftOrder(
416
- client,
417
- customerBizplace,
418
- bodyReq,
419
- bundleInfo,
420
- domain,
421
- context,
422
- orderInventories,
423
- tx
424
- )
425
- }
426
- return
427
- }
428
- }
429
- inventories = inventories
430
- .map(inventory => {
431
- return {
432
- ...inventory,
433
- remainQty: inventory.qty - inventory.lockedQty,
434
- remainUomValue: inventory.uomValue - inventory.lockedUomValue
435
- }
436
- })
437
- .filter(inventory => inventory.remainQty > 0)
438
-
439
- let targetInventories: WorksheetDetail[]
440
- try {
441
- targetInventories = _composeTargetInventories(context, product, orderInventory, inventories)
442
- } catch (e) {
443
- if (requiredDraft) {
444
- context.body = {
445
- result: await createDraftOrder(
446
- client,
447
- customerBizplace,
448
- bodyReq,
449
- bundleInfo,
450
- domain,
451
- context,
452
- orderInventories,
453
- tx
454
- )
455
- }
456
- return
457
- } else {
458
- targetInventories = _composeTargetInventories(context, product, orderInventory, inventories)
459
- }
460
- }
461
- if (!targetInventories?.length) {
462
- context.throw(404, t(`error.no inventories found`))
463
- }
464
- targetInventories.map(item => {
465
- finalOrderInventories.push(item.targetInventory)
466
- })
467
- } else {
468
- let filters: any = [
469
- {
470
- name: 'productName',
471
- operator: 'eq',
472
- value: orderInventory.product.sku
473
- },
474
- {
475
- name: 'bizplace_id',
476
- operator: 'eq',
477
- value: customerBizplaceId
478
- }
479
- ]
480
-
481
- // 2. get all inventory data according to SKU and batchId
482
- const response = await client.query({
483
- query: gql`
484
- query inventoryProductGroup($filters: [Filter]) {
485
- inventoryProductGroup(filters: $filters) {
486
- items {
487
- productId
488
- productSKU
489
- productName
490
- batchId
491
- packingType
492
- remainQty
493
- remainUomValue
494
- }
495
- total
496
- }
497
- }
498
- `,
499
- variables: { filters },
500
- context: {
501
- ...context,
502
- state: {
503
- ...context.state,
504
- bizplace: customerBizplace,
505
- domain
506
- }
507
- }
508
- } as any)
509
-
510
- if (!response.errors) {
511
- const items = response.data.inventoryProductGroup.items || []
512
- if (requiredDraft) {
513
- if (!items?.length) {
514
- context.body = {
515
- result: await createDraftOrder(
516
- client,
517
- customerBizplace,
518
- bodyReq,
519
- bundleInfo,
520
- domain,
521
- context,
522
- orderInventories,
523
- tx
524
- )
525
- }
526
- return
527
- }
528
- }
529
-
530
- items.forEach(async item => {
531
- if (requiredDraft) {
532
- if (orderInventory.releaseQty > item.remainQty || orderInventory.releaseQty <= 0) {
533
- context.body = {
534
- result: await createDraftOrder(
535
- client,
536
- customerBizplace,
537
- bodyReq,
538
- bundleInfo,
539
- domain,
540
- context,
541
- orderInventories,
542
- tx
543
- )
544
- }
545
- return
546
- }
547
- }
548
-
549
- // 3. add more information to orderInventory
550
- orderInventory.product = { id: item.productId }
551
- orderInventory.packingType = item.packingType
552
- orderInventory.batchId = item.batchId
553
- orderInventory.type = ORDER_TYPES.RELEASE_OF_GOODS
554
- orderInventory.releaseUomValue = orderInventory.releaseQty * orderInventory.uomValue
555
- orderInventory.uom = item.uom
556
- })
557
-
558
- delete orderInventory.uomValue
559
- finalOrderInventories.push(orderInventory)
560
- } else {
561
- if (requiredDraft) {
562
- context.body = {
563
- result: await createDraftOrder(
564
- client,
565
- customerBizplace,
566
- bodyReq,
567
- bundleInfo,
568
- domain,
569
- context,
570
- orderInventories,
571
- tx
572
- )
573
- }
574
- return
575
- }
576
- }
577
- }
578
- }
163
+ if (errorList.length > 0) {
164
+ throw new ValidationError({
165
+ errorCode: 'INSUFFICIENT_STOCK',
166
+ message: 'insufficient stock',
167
+ detail: errorList.join(', ')
168
+ })
169
+ }
170
+ // assignment
171
+ console.time('assign')
172
+ let assignedOrderProducts = await assignToInventory(massagedData.orderProducts, customerBizplace, context, tx)
173
+ console.timeEnd('assign')
174
+
175
+ // create order
176
+ console.time('save')
177
+ let result = await createReleaseGood(releaseGood, assignedOrderProducts, customerBizplace, context, tx)
178
+ console.timeEnd('save')
179
+
180
+ const sellercraft: Sellercraft = await tx
181
+ .getRepository(Sellercraft)
182
+ .findOne({ domain: customerBizplace.domain })
183
+
184
+ if (sellercraft) {
185
+ const sellercraftCtrl: SellercraftController = new SellercraftController(tx, domain, user)
186
+ await sellercraftCtrl.packOrder(sellercraft, {
187
+ ...result,
188
+ orderProducts: result.orderProducts
189
+ })
579
190
  }
580
191
 
581
- context.body = {
582
- result: (
583
- await client.mutate({
584
- mutation: gql`
585
- mutation addReleaseGood(
586
- $releaseGood: NewReleaseGood
587
- $shippingOrder: ShippingOrderPatch
588
- $file: Upload
589
- ) {
590
- addReleaseGood(releaseGood: $releaseGood, shippingOrder: $shippingOrder, file: $file) {
591
- id
592
- name
593
- refNo
594
- status
595
- truckNo
596
- ownTransport
597
- crossDocking
598
- marketplaceOrderStatus
599
- billingAddress
600
- deliveryAddress1
601
- deliveryAddress2
602
- deliveryAddress3
603
- deliveryAddress4
604
- deliveryAddress5
605
- attentionTo
606
- attentionCompany
607
- city
608
- state
609
- postalCode
610
- country
611
- phone1
612
- phone2
613
- email
614
- transporter
615
- trackingNo
616
- airwayBill
617
- invoice
618
- type
619
- storeName
620
- storeId
621
- routeId
622
- stopId
623
- arrivalNotice {
624
- name
625
- }
626
- exportOption
627
- releaseDate
628
- collectionOrderNo
629
- bizplace {
630
- name
631
- }
632
- orderInventories {
633
- product {
634
- sku
635
- name
636
- description
637
- }
638
- batchId
639
- packingType
640
- packingSize
641
- releaseQty
642
- releaseUomValue
643
- }
644
- orderVass {
645
- vas {
646
- name
647
- operationGuide
648
- operationGuideType
649
- }
650
- set
651
- targetType
652
- targetBatchId
653
- targetProduct {
654
- name
655
- description
656
- }
657
- packingType
658
- qty
659
- uomValue
660
- otherTarget
661
- description
662
- remark
663
- status
664
- operationGuide
665
- }
666
- }
667
- }
668
- `,
669
- variables: {
670
- releaseGood: {
671
- customerBizplaceId: customerBizplace.id,
672
- courierOption: bodyReq.courierOption,
673
- collectionOrderNo: bodyReq.collectionOrderNo,
674
- exportOption: bodyReq.exportOption,
675
- orderInventories: finalOrderInventories,
676
- ownTransport: bodyReq.ownTransport,
677
- packingOption: bodyReq.packingOption,
678
- refNo: bodyReq.refNo,
679
- refNo2: bodyReq?.refNo2,
680
- releaseDate: bodyReq.releaseDate,
681
- type: bodyReq.type,
682
- marketplaceOrderStatus: bodyReq?.marketplaceOrderStatus,
683
- remark: bodyReq?.remark || null,
684
- billingAddress: bodyReq?.billTo?.billingAddress || null,
685
- deliveryAddress1: bodyReq?.deliverTo?.deliveryAddress1 || null,
686
- deliveryAddress2: bodyReq?.deliverTo?.deliveryAddress2 || null,
687
- deliveryAddress3: bodyReq?.deliverTo?.deliveryAddress3 || null,
688
- deliveryAddress4: bodyReq?.deliverTo?.deliveryAddress4 || null,
689
- deliveryAddress5: bodyReq?.deliverTo?.deliveryAddress5 || null,
690
- attentionTo: bodyReq?.deliverTo?.attentionTo || null,
691
- attentionCompany: bodyReq?.deliverTo?.attentionCompany || null,
692
- city: bodyReq?.deliverTo?.city || null,
693
- state: bodyReq?.deliverTo?.state || null,
694
- postalCode: bodyReq?.deliverTo?.postalCode || null,
695
- country: bodyReq?.deliverTo?.country || null,
696
- phone1: bodyReq?.deliverTo?.phone1 || null,
697
- phone2: bodyReq?.deliverTo?.phone2 || null,
698
- email: bodyReq?.deliverTo?.email || null,
699
- transporter: bodyReq?.transporter,
700
- trackingNo: bodyReq?.trackingNo,
701
- airwayBill: bodyReq?.airwayBill,
702
- invoice: bodyReq?.invoice,
703
- bundleInfo: JSON.stringify(bundleInfo),
704
- storeName: bodyReq?.storeName,
705
- storeId: bodyReq?.storeId,
706
- routeId: bodyReq?.routeId,
707
- stopId: bodyReq?.stopId
708
- }
709
- },
710
- context: {
711
- ...context,
712
- hasUpload: true,
713
- state: {
714
- ...context.state,
715
- customerBizplace,
716
- domain,
717
- type: 'api'
718
- }
719
- }
720
- } as any)
721
- ).data
192
+ let addReleaseGood = {
193
+ id: result.id,
194
+ name: result.name,
195
+ refNo: result.refNo,
196
+ refNo2: result.refNo2,
197
+ refNo3: result.refNo3,
198
+ status: result.status,
199
+ truckNo: result.truckNo,
200
+ ownTransport: result.ownTransport,
201
+ crossDocking: result.crossDocking,
202
+ marketplaceOrderStatus: result.marketplaceOrderStatus,
203
+ billingAddress: result.billingAddress,
204
+ deliveryAddress1: result.deliveryAddress1,
205
+ deliveryAddress2: result.deliveryAddress2,
206
+ deliveryAddress3: result.deliveryAddress3,
207
+ deliveryAddress4: result.deliveryAddress4,
208
+ deliveryAddress5: result.deliveryAddress5,
209
+ attentionTo: result.attentionTo,
210
+ attentionCompany: result.attentionCompany,
211
+ city: result.city,
212
+ state: result.state,
213
+ postalCode: result.postalCode,
214
+ country: result.country,
215
+ phone1: result.phone1,
216
+ phone2: result.phone2,
217
+ email: result.email,
218
+ transporter: result.transporter,
219
+ trackingNo: result.trackingNo,
220
+ airwayBill: result.airwayBill,
221
+ invoice: result.invoice,
222
+ type: result.type,
223
+ storeName: result.storeName,
224
+ storeId: result.storeId,
225
+ routeId: result.routeId,
226
+ stopId: result.stopId,
227
+ exportOption: result.exportOption,
228
+ releaseDate: result.releaseDate,
229
+ collectionOrderNo: result.collectionOrderNo,
230
+ bizplace: { name: result.bizplace.name },
231
+ domain: { name: result.domain.name },
232
+ orderInventories: result.orderInventories
722
233
  }
234
+ // return body
235
+ context.body = { result: { addReleaseGood } }
723
236
 
724
237
  createPayloadLog(
725
238
  customerBizplaceId,
@@ -738,13 +251,20 @@ router.post(
738
251
  context,
739
252
  PayloadType.INGESTION
740
253
  )
741
- throw error
254
+
255
+ if (error.errorCode) {
256
+ if (error.errorCode == 'INSUFFICIENT_STOCK' && requiredDraft) {
257
+ await createDraftOrder(releaseGood, massagedData.orderProducts, context)
258
+ }
259
+ }
260
+ logger.error(`add-release-order: ${JSON.stringify(error)}`)
261
+ context.throw(400, context.t(error))
742
262
  }
743
263
  })
744
264
  }
745
265
  )
746
266
 
747
- export async function checkMandatoryKey(context, params) {
267
+ async function checkMandatoryKey(context, params) {
748
268
  let noparams = ['releaseDate', 'exportOption'].filter(param => !(param in params))
749
269
 
750
270
  if ('ownTransport' in params) {
@@ -792,172 +312,690 @@ export async function checkMandatoryKey(context, params) {
792
312
  return params
793
313
  }
794
314
 
795
- export function _composeTargetInventories(context, product, record, inventories) {
796
- const { t } = context
797
- let leftReleaseQty = record.releaseQty
798
- let leftReleaseUomValue = record.releaseUomValue
799
- let compReleaseQty = 0
800
- let compReleaseUomValue = 0
801
- let totalInventoryQty: number = 0
802
-
803
- inventories.map(inventory => {
804
- totalInventoryQty += inventory.remainQty
805
- })
315
+ async function checkExistingOrder(releaseGood, bizplace, context, tx) {
316
+ const { domain, user }: { domain: Domain; user: User } = context.state
317
+ const refNo: string = releaseGood.refNo
318
+ const refNo2: string = releaseGood.refNo2
319
+ const refNo3: string = releaseGood.refNo3
320
+
321
+ if (releaseGood.type == 'b2c') {
322
+ const foundDraftReleaseGood: DraftReleaseGood = await tx.getRepository(DraftReleaseGood).findOne({
323
+ where: {
324
+ domain,
325
+ refNo: refNo || null,
326
+ refNo2: refNo2 || null,
327
+ refNo3: refNo3 || null,
328
+ status: Not(In([DRAFT_RELEASE_ORDER_STATUS.CANCELLED]))
329
+ }
330
+ })
806
331
 
807
- if (totalInventoryQty < record.releaseQty) {
808
- context.throw(404, t(`error.invalid release qty for ${product?.sku}`)) //
809
- }
332
+ if (foundDraftReleaseGood) {
333
+ throw new ValidationError({
334
+ errorCode: 'EXISTING_ORDER',
335
+ message: 'existing draft release order found',
336
+ detail: foundDraftReleaseGood.name
337
+ })
338
+ }
810
339
 
811
- let targetInventories = []
812
- let idx = 0
813
- while (compReleaseQty < record.releaseQty) {
814
- const inv = inventories[idx]
340
+ const sellercraft: Sellercraft = await tx.getRepository(Sellercraft).findOne({ domain: bizplace.domain })
815
341
 
816
- if (inv.remainQty > leftReleaseQty) {
817
- compReleaseQty += leftReleaseQty
818
- compReleaseUomValue += leftReleaseUomValue
342
+ if (sellercraft) {
343
+ const sellercraftCtrl: SellercraftController = new SellercraftController(tx, bizplace.domain, user)
344
+ await sellercraftCtrl.checkExistingReleaseGood(sellercraft, releaseGood)
345
+ } else {
346
+ const foundReleaseGood: ReleaseGood = await tx.getRepository(ReleaseGood).findOne({
347
+ where: {
348
+ domain: domain,
349
+ refNo,
350
+ status: Not(In([ORDER_STATUS.CANCELLED, ORDER_STATUS.PENDING_CANCEL]))
351
+ },
352
+ relations: ['bizplace', 'bizplace.domain', 'bizplace.company', 'bizplace.company.domain']
353
+ })
819
354
 
820
- targetInventories.push({
821
- targetInventory: {
822
- inventory: { id: inv.id },
823
- packingType: inv.packingType,
824
- batchId: inv.batchId,
825
- releaseQty: leftReleaseQty,
826
- releaseUomValue: leftReleaseQty * record.uomValue,
827
- uom: inv.uom,
828
- product: { id: product.id },
829
- type: ORDER_TYPES.RELEASE_OF_GOODS
355
+ if (foundReleaseGood) {
356
+ const customerCompanyDomain: Domain = foundReleaseGood.bizplace.company.domain
357
+ const marketplaceOrder: MarketplaceOrder = await tx.getRepository(MarketplaceOrder).findOne({
358
+ where: { orderNo: refNo, domain: customerCompanyDomain }
359
+ })
360
+
361
+ // Need to restructure the validation
362
+ if (marketplaceOrder?.isSplitted) {
363
+ const refNo2: string = releaseGood.refNo2
364
+ const foundSplittedReleaseGood = await tx.getRepository(ReleaseGood).findOne({
365
+ where: {
366
+ domain,
367
+ refNo2,
368
+ status: Not(In([ORDER_STATUS.CANCELLED, ORDER_STATUS.PENDING_CANCEL]))
369
+ },
370
+ relations: ['bizplace', 'bizplace.domain', 'bizplace.company', 'bizplace.company.domain']
371
+ })
372
+
373
+ if (foundSplittedReleaseGood)
374
+ throw new ValidationError({
375
+ errorCode: 'EXISTING_ORDER',
376
+ message: 'existing release order found',
377
+ detail: foundSplittedReleaseGood.name
378
+ })
379
+ } else {
380
+ throw new ValidationError({
381
+ errorCode: 'EXISTING_ORDER',
382
+ message: 'existing release order found',
383
+ detail: foundReleaseGood.name
384
+ })
830
385
  }
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ async function createDraftOrder(releaseGood, orderProducts, context) {
392
+ return await getConnection().transaction(async (tx: EntityManager) => {
393
+ return await createDraftReleaseGoodFunction(null, releaseGood, orderProducts, null, [], context, tx)
394
+ })
395
+ }
396
+
397
+ async function createReleaseGood(
398
+ releaseGood: any,
399
+ orderProducts: OrderProduct[],
400
+ bizplace: Bizplace,
401
+ context: any,
402
+ tx?: EntityManager
403
+ ): Promise<ReleaseGood> {
404
+ try {
405
+ const { domain, user }: { domain: Domain; user: User } = context.state
406
+ const settingRepo: Repository<Setting> = tx?.getRepository(Setting) || getRepository(Setting)
407
+
408
+ if (orderProducts.length <= 0) {
409
+ throw new ValidationError({
410
+ errorCode: 'ITEM_NOT_FOUND',
411
+ message: 'no order items found',
412
+ detail: JSON.stringify(releaseGood)
831
413
  })
832
- } else {
833
- compReleaseQty += inv.remainQty
834
- compReleaseUomValue += inv.remainUomValue
835
- leftReleaseQty -= inv.remainQty
836
- leftReleaseUomValue -= inv.remainUomValue
414
+ }
837
415
 
838
- targetInventories.push({
839
- targetInventory: {
840
- inventory: { id: inv.id },
841
- packingType: inv.packingType,
842
- batchId: inv.batchId,
843
- releaseQty: inv.remainQty,
844
- releaseUomValue: inv.remainUomValue,
845
- uom: inv.uom,
846
- product: { id: product.id },
847
- type: ORDER_TYPES.RELEASE_OF_GOODS
416
+ let newReleaseGood: ReleaseGood = new ReleaseGood()
417
+
418
+ // find RO number rule setting
419
+ const roNoSetting: Setting = await settingRepo.findOne({
420
+ where: {
421
+ domain,
422
+ name: ORDER_NUMBER_SETTING_KEY.RO_NUMBER_RULE
423
+ }
424
+ })
425
+
426
+ newReleaseGood = {
427
+ ...newReleaseGood,
428
+ name: roNoSetting
429
+ ? await generateId({ domain, type: ORDER_NUMBER_RULE_TYPE.RO_NUMBER, seed: {} })
430
+ : OrderNoGenerator.releaseGood(),
431
+ domain: domain,
432
+ bizplace: bizplace,
433
+ collectionOrderNo: releaseGood.collectionOrderNo,
434
+ courierOption: releaseGood.courierOption,
435
+ exportOption: releaseGood.exportOption,
436
+ ownTransport: releaseGood.ownTransport,
437
+ packingOption: releaseGood.packingOption,
438
+ marketplaceOrderStatus: releaseGood?.marketplaceOrderStatus || null,
439
+ billingAddress: releaseGood?.billingAddress || null,
440
+ deliveryAddress1: releaseGood?.deliveryAddress1 || null,
441
+ deliveryAddress2: releaseGood?.deliveryAddress2 || null,
442
+ deliveryAddress3: releaseGood?.deliveryAddress3 || null,
443
+ deliveryAddress4: releaseGood?.deliveryAddress4 || null,
444
+ deliveryAddress5: releaseGood?.deliveryAddress5 || null,
445
+ attentionTo: releaseGood?.attentionTo || null,
446
+ attentionCompany: releaseGood?.attentionCompany || null,
447
+ city: releaseGood?.city || null,
448
+ state: releaseGood?.state || null,
449
+ postalCode: releaseGood?.postalCode || null,
450
+ country: releaseGood?.country || null,
451
+ phone1: releaseGood?.phone1 || null,
452
+ phone2: releaseGood?.phone2 || null,
453
+ email: releaseGood?.email || null,
454
+ transporter: releaseGood?.transporter,
455
+ trackingNo: releaseGood?.trackingNo,
456
+ airwayBill: releaseGood?.airwayBill,
457
+ invoice: releaseGood?.invoice,
458
+ refNo: releaseGood.refNo,
459
+ refNo2: releaseGood.refNo2 || null,
460
+ refNo3: releaseGood.refNo3 || null,
461
+ remark: releaseGood?.remark || null,
462
+ releaseDate: releaseGood.releaseDate,
463
+ truckNo: releaseGood.truckNo,
464
+ type: releaseGood?.type ? releaseGood.type : 'b2b',
465
+ status: ORDER_STATUS.PENDING_WORKSHEET,
466
+ storeName: releaseGood?.storeName,
467
+ storeId: releaseGood?.storeId,
468
+ routeId: releaseGood?.routeId,
469
+ stopId: releaseGood?.stopId,
470
+ noOfItems: orderProducts.length,
471
+ creator: user,
472
+ updater: user,
473
+ acceptedBy: user,
474
+ acceptedAt: new Date()
475
+ }
476
+
477
+ let resultReleaseGood = await tx.getRepository(ReleaseGood).save(newReleaseGood)
478
+ let combinedOrderInventories: OrderInventory[] = []
479
+ let savedOrderProducts = await Promise.all(
480
+ orderProducts.map(async orderProduct => {
481
+ let orderInventories: OrderInventory[] = orderProduct.orderInventories
482
+ orderProduct = Object.assign(new OrderProduct(), {
483
+ ...orderProduct,
484
+ name: uuidv4(),
485
+ domain: domain,
486
+ bizplace: bizplace,
487
+ releaseGood: resultReleaseGood,
488
+ type: ORDER_TYPES.RELEASE_OF_GOODS,
489
+ status: ORDER_PRODUCT_STATUS.ASSIGNED
490
+ })
491
+ let result = await tx.getRepository(OrderProduct).save(orderProduct)
492
+
493
+ for (let oiIdx = 0; oiIdx < orderInventories.length; oiIdx++) {
494
+ let orderInventory = Object.assign(new OrderInventory(), {
495
+ ...orderInventories[oiIdx],
496
+ name: uuidv4(),
497
+ domain: domain,
498
+ bizplace: bizplace,
499
+ releaseGood: resultReleaseGood,
500
+ type: ORDER_TYPES.RELEASE_OF_GOODS,
501
+ status: ORDER_PRODUCT_STATUS.ASSIGNED
502
+ })
503
+ await tx.getRepository(OrderInventory).save(orderInventory)
504
+
505
+ combinedOrderInventories.push({
506
+ product: { name: orderInventory.product.name, sku: orderInventory.product.sku },
507
+ batchId: orderInventory.batchId,
508
+ packingType: orderInventory.packingType,
509
+ packingSize: orderInventory.packingSize,
510
+ releaseQty: orderInventory.releaseQty,
511
+ releaseUomValue: orderInventory.releaseUomValue
512
+ })
848
513
  }
514
+
515
+ return result
849
516
  })
850
- }
517
+ )
851
518
 
852
- idx++
853
- }
519
+ newReleaseGood.orderProducts = savedOrderProducts
520
+ newReleaseGood.orderInventories = combinedOrderInventories
854
521
 
855
- return targetInventories
522
+ return newReleaseGood
523
+ } catch (error) {
524
+ throw error
525
+ }
856
526
  }
857
527
 
858
- function combineItems(inventoryList) {
859
- let mappedList = []
528
+ async function massageOrderItems(
529
+ releaseGood,
530
+ inputOrderProducts,
531
+ context
532
+ ): Promise<{ orderProducts: OrderProduct[]; combinedItems: OrderProduct[] }> {
533
+ const { t } = context
534
+ try {
535
+ let orderProducts: OrderProduct[] = []
536
+ let errorList = []
537
+ //combine any repeated skus
538
+ inputOrderProducts = inputOrderProducts.reduce((acc, itm, idx) => {
539
+ let { sku, refCode } = itm.product
540
+ let { packingType, packingSize, batchId, releaseQty } = itm
541
+
542
+ if (!sku && !refCode) {
543
+ errorList.push(
544
+ JSON.stringify({
545
+ code: 'INVALID_DATA',
546
+ message: 'sku or refCode not found',
547
+ sku: sku,
548
+ refCode: refCode,
549
+ packingType: packingType?.trim(),
550
+ packingSize: packingSize,
551
+ qty: releaseQty
552
+ })
553
+ )
554
+ }
860
555
 
861
- inventoryList.map(e => {
862
- let item = mappedList.find(i => i.product.id === e.product.id)
863
- if (item) {
864
- mappedList = mappedList.filter(m => m != item)
865
- item.releaseQty += e.releaseQty
866
- mappedList.push(item)
867
- } else {
868
- mappedList.push(e)
556
+ if (releaseQty <= 0) {
557
+ errorList.push(
558
+ JSON.stringify({
559
+ code: 'INVALID_DATA',
560
+ message: 'negative stock request',
561
+ sku: sku,
562
+ refCode: refCode,
563
+ packingType: packingType?.trim(),
564
+ packingSize: packingSize,
565
+ qty: releaseQty
566
+ })
567
+ )
568
+ }
569
+
570
+ let existingIndex = acc.findIndex(
571
+ itm =>
572
+ itm?.sku == sku?.trim() &&
573
+ itm?.refCode == refCode?.trim() &&
574
+ itm?.packingType == packingType?.trim() &&
575
+ itm?.packingSize == packingSize
576
+ )
577
+
578
+ if (existingIndex >= 0 && releaseQty > 0) {
579
+ acc[existingIndex].releaseQty = acc[existingIndex].releaseQty + releaseQty
580
+ } else {
581
+ acc.push({
582
+ sku: sku?.trim(),
583
+ refCode: refCode?.trim(),
584
+ packingType: packingType?.trim(),
585
+ packingSize: packingSize,
586
+ releaseQty: releaseQty
587
+ })
588
+ }
589
+
590
+ return acc
591
+ }, [])
592
+
593
+ if (errorList.length > 0) {
594
+ throw new ValidationError({
595
+ errorCode: 'INVALID_DATA',
596
+ message: 'invalid data',
597
+ detail: errorList.join(', ')
598
+ })
869
599
  }
870
- })
871
600
 
872
- return mappedList
873
- }
601
+ //find customer sku in system
602
+ await Promise.all(
603
+ inputOrderProducts.map(async inputOrderItem => {
604
+ const sku: string = inputOrderItem.sku
605
+ const refCode: string = inputOrderItem.refCode
606
+ const packingType: string = inputOrderItem.packingType
607
+ const packingSize: string = inputOrderItem.packingSize
608
+
609
+ const qb: SelectQueryBuilder<ProductDetail> = getRepository(ProductDetail)
610
+ .createQueryBuilder('PD')
611
+ .innerJoinAndSelect('PD.product', 'PROD')
612
+ .where('PROD.bizplace_id = :bizplaceId', { bizplaceId: releaseGood.companyBizplace.id })
613
+
614
+ if (!sku && !refCode) {
615
+ context.throw(404, t('error.sku or refCode not found'))
616
+ }
617
+
618
+ if (refCode) {
619
+ qb.andWhere('PD.ref_code = :refCode', { refCode })
620
+ }
621
+
622
+ if (sku) {
623
+ qb.andWhere('PROD.sku = :sku', { sku })
624
+ }
874
625
 
875
- async function createDraftOrder(client, customerBizplace, bodyReq, bundleInfo, domain, context, orderInventories, tx) {
876
- return (
877
- await client.mutate({
878
- mutation: gql`
879
- mutation upsertDraftReleaseGood(
880
- $draftReleaseGood: DraftReleaseGoodPatch!
881
- $orderProducts: [OrderProductPatch!]!
882
- ) {
883
- upsertDraftReleaseGood(draftReleaseGood: $draftReleaseGood, orderProducts: $orderProducts) {
884
- id
885
- name
886
- refNo
887
- refNo2
888
- status
889
- truckNo
890
- ownTransport
891
- marketplaceOrderStatus
892
- billingAddress
893
- deliveryAddress1
894
- deliveryAddress2
895
- deliveryAddress3
896
- deliveryAddress4
897
- deliveryAddress5
898
- attentionTo
899
- attentionCompany
900
- city
901
- state
902
- postalCode
903
- country
904
- phone1
905
- phone2
906
- email
907
- type
908
- exportOption
909
- releaseDate
910
- collectionOrderNo
911
- bizplace {
912
- name
626
+ if (packingType) {
627
+ qb.andWhere('PD.packing_type = :packingType', { packingType })
628
+ }
629
+
630
+ if ((!packingType && !refCode) || !packingSize) {
631
+ qb.andWhere('PD.is_default = :isDefault', { isDefault: true })
632
+ }
633
+
634
+ const productDetail: ProductDetail = await qb.getOne()
635
+
636
+ // order Product based on product detail
637
+ if (productDetail) {
638
+ let newOrderProduct: OrderInventory = Object.assign({}, inputOrderItem)
639
+ newOrderProduct = {
640
+ ...newOrderProduct,
641
+ product: productDetail.product,
642
+ productDetail: productDetail,
643
+ packingType: productDetail.packingType,
644
+ packingSize: packingSize || productDetail.packingSize,
645
+ uom: productDetail.uom,
646
+ uomValue: productDetail.uomValue,
647
+ refCode: inputOrderItem.refCode,
648
+ releaseQty: inputOrderItem.releaseQty,
649
+ releaseUomValue: inputOrderItem.releaseQty * (productDetail.uomValue || 1),
650
+ packQty: 0,
651
+ actualPackQty: 0,
652
+ palletQty: 0,
653
+ actualPalletQty: 0,
654
+ type: 'RELEASE_OF_GOODS',
655
+ status: ORDER_PRODUCT_STATUS.ASSIGNED,
656
+ batchId: '-'
657
+ }
658
+
659
+ newOrderProduct.orderInventories = [
660
+ {
661
+ ...newOrderProduct,
662
+ packQty: 0,
663
+ actualPackQty: 0,
664
+ palletQty: 0,
665
+ actualPalletQty: 0,
666
+ type: 'RELEASE_OF_GOODS',
667
+ status: ORDER_INVENTORY_STATUS.PENDING_WORKSHEET
668
+ }
669
+ ]
670
+
671
+ orderProducts.push(newOrderProduct)
672
+ }
673
+
674
+ if (!productDetail) {
675
+ const qb: SelectQueryBuilder<ProductDetail> = getRepository(ProductBundle)
676
+ .createQueryBuilder('PB')
677
+ .innerJoinAndSelect('PB.productBundleSettings', 'PBS')
678
+ .innerJoinAndSelect('PBS.productDetail', 'PBD')
679
+ .innerJoinAndSelect('PBD.product', 'PROD')
680
+ .where('PB.bizplace_id = :bizplaceId', { bizplaceId: releaseGood.companyBizplace.id })
681
+
682
+ if (!sku && !refCode) {
683
+ context.throw(404, t('error.sku or refCode not found'))
684
+ }
685
+
686
+ if (refCode) {
687
+ qb.andWhere('PB.ref_code = :refCode', { refCode })
688
+ }
689
+
690
+ if (sku) {
691
+ qb.andWhere('PB.sku = :sku', { sku })
692
+ }
693
+
694
+ if (packingType) {
695
+ qb.andWhere('PB.packing_type = :packingType', { packingType })
696
+ }
697
+
698
+ const foundProductBundle: ProductBundle = await qb.getOne()
699
+
700
+ if (foundProductBundle) {
701
+ let orderProduct = {
702
+ productBundle: foundProductBundle,
703
+ refCode,
704
+ packingType: foundProductBundle.packingType,
705
+ packingSize: packingSize || 1,
706
+ uom: inputOrderItem?.uom,
707
+ releaseQty: inputOrderItem.releaseQty,
708
+ releaseUomValue: inputOrderItem.releaseQty,
709
+ packQty: 0,
710
+ actualPackQty: 0,
711
+ palletQty: 0,
712
+ actualPalletQty: 0,
713
+ orderInventories: [],
714
+ type: 'RELEASE_OF_GOODS',
715
+ status: ORDER_PRODUCT_STATUS.ASSIGNED,
716
+ batchId: '-'
717
+ }
718
+
719
+ const bundleProductSettings: any[] = foundProductBundle.productBundleSettings
720
+ bundleProductSettings.map(bundleProductSetting => {
721
+ const productDetailBundle: ProductDetail = bundleProductSetting.productDetail
722
+
723
+ let newOrderInventory: OrderInventory = {
724
+ product: productDetailBundle.product,
725
+ productDetail: productDetailBundle,
726
+ sku: productDetailBundle.product.sku,
727
+ packingType: productDetailBundle.packingType,
728
+ packingSize: productDetailBundle.packingSize,
729
+ uom: productDetailBundle.uom,
730
+ packQty: 0,
731
+ actualPackQty: 0,
732
+ palletQty: 0,
733
+ actualPalletQty: 0,
734
+ releaseQty: inputOrderItem.releaseQty * bundleProductSetting.bundleQty,
735
+ releaseUomValue:
736
+ inputOrderItem.releaseQty *
737
+ bundleProductSetting.bundleQty *
738
+ (productDetailBundle.uomValue <= 0 ? 1 : productDetailBundle.uomValue),
739
+ type: 'RELEASE_OF_GOODS',
740
+ status: ORDER_INVENTORY_STATUS.PENDING_WORKSHEET
741
+ }
742
+
743
+ orderProduct.orderInventories.push(newOrderInventory)
744
+ })
745
+ orderProducts.push(orderProduct)
746
+ } else {
747
+ if (sku && refCode) {
748
+ context.throw(404, t(`error.sku ${sku} and refCode ${refCode} not exist in product / bundle master`))
749
+ }
750
+
751
+ if (sku) {
752
+ context.throw(404, t(`error.sku ${sku} not exist in product / bundle master`))
753
+ }
754
+
755
+ if (refCode) {
756
+ context.throw(404, t(`error.refCode ${refCode} not exist in product / bundle master`))
913
757
  }
914
758
  }
915
759
  }
916
- `,
917
- variables: {
918
- draftReleaseGood: {
919
- bizplace: {
920
- id: customerBizplace.id
921
- },
922
- courierOption: bodyReq.courierOption,
923
- collectionOrderNo: bodyReq.collectionOrderNo,
924
- exportOption: bodyReq.exportOption,
925
- ownTransport: bodyReq.ownTransport,
926
- packingOption: bodyReq.packingOption,
927
- refNo: bodyReq.refNo,
928
- refNo2: bodyReq?.refNo2,
929
- releaseDate: bodyReq.releaseDate,
930
- type: bodyReq.type,
931
- marketplaceOrderStatus: bodyReq?.marketplaceOrderStatus,
932
- remark: bodyReq?.remark || null,
933
- billingAddress: bodyReq?.billTo?.billingAddress || null,
934
- deliveryAddress1: bodyReq?.deliverTo?.deliveryAddress1 || null,
935
- deliveryAddress2: bodyReq?.deliverTo?.deliveryAddress2 || null,
936
- deliveryAddress3: bodyReq?.deliverTo?.deliveryAddress3 || null,
937
- deliveryAddress4: bodyReq?.deliverTo?.deliveryAddress4 || null,
938
- deliveryAddress5: bodyReq?.deliverTo?.deliveryAddress5 || null,
939
- attentionTo: bodyReq?.deliverTo?.attentionTo || null,
940
- attentionCompany: bodyReq?.deliverTo?.attentionCompany || null,
941
- city: bodyReq?.deliverTo?.city || null,
942
- state: bodyReq?.deliverTo?.state || null,
943
- postalCode: bodyReq?.deliverTo?.postalCode || null,
944
- country: bodyReq?.deliverTo?.country || null,
945
- phone1: bodyReq?.deliverTo?.phone1 || null,
946
- phone2: bodyReq?.deliverTo?.phone2 || null,
947
- email: bodyReq?.deliverTo?.email || null
760
+ })
761
+ )
762
+
763
+ //combine any repeated skus
764
+ let combinedItems: OrderProduct[] = orderProducts.reduce((acc, op, idx) => {
765
+ let oi = op?.orderInventories || []
766
+ oi.forEach(itm => {
767
+ let { sku, refCode } = itm.product
768
+ let { packingType, packingSize, batchId, releaseQty, product, productDetail, uom } = itm
769
+
770
+ if (!sku && !refCode) {
771
+ context.throw(404, t('error.sku or refCode not found'))
772
+ }
773
+ let existingIndex = acc.findIndex(
774
+ itm =>
775
+ itm?.sku == sku?.trim() &&
776
+ itm?.refCode == refCode?.trim() &&
777
+ itm?.packingType == packingType?.trim() &&
778
+ itm?.packingSize == packingSize &&
779
+ itm?.batchId == batchId?.trim() &&
780
+ itm?.uom == uom?.trim()
781
+ )
782
+
783
+ if (existingIndex >= 0 && releaseQty > 0) {
784
+ acc[existingIndex].releaseQty = acc[existingIndex].releaseQty + releaseQty
785
+ acc[existingIndex].releaseUomValue =
786
+ acc[existingIndex].releaseUomValue + releaseQty * (productDetail.uomValue <= 0 ? 1 : productDetail.uomValue)
787
+ } else {
788
+ let updateObj = {
789
+ sku: sku?.trim(),
790
+ packingType: packingType?.trim(),
791
+ packingSize: packingSize,
792
+ uom: uom?.trim(),
793
+ releaseQty: releaseQty,
794
+ releaseUomValue: releaseQty * (productDetail.uomValue <= 0 ? 1 : productDetail.uomValue),
795
+ product,
796
+ productDetail
797
+ }
798
+
799
+ if (refCode) updateObj['refCode'] = refCode.trim()
800
+ updateObj['batchId'] = batchId ? batchId?.trim() : '-'
801
+
802
+ acc.push(updateObj)
803
+ }
804
+ })
805
+ return acc
806
+ }, [])
807
+
808
+ return { orderProducts, combinedItems }
809
+ } catch (error) {
810
+ throw error
811
+ }
812
+ }
813
+
814
+ async function assignToInventory(orderProducts: OrderProduct[], customerBizplace, context, tx) {
815
+ const { client, domain, user } = context.state
816
+ for (let opIdx = 0; opIdx < orderProducts.length; opIdx++) {
817
+ let orderProduct = orderProducts[opIdx]
818
+
819
+ let assignedOrderInventories = []
820
+ for (let oiIdx = 0; oiIdx < orderProducts[opIdx].orderInventories.length; oiIdx++) {
821
+ const orderInventory = orderProduct.orderInventories[oiIdx]
822
+ let sortings: any = []
823
+
824
+ switch (orderInventory.product.pickingStrategy) {
825
+ case 'LIFO':
826
+ sortings.push({ name: 'iv.created_at', desc: true })
827
+
828
+ break
829
+ case 'FEFO':
830
+ sortings.push({ name: 'iv.expiration_date', desc: false })
831
+ sortings.push({ name: 'iv.created_at', desc: false })
832
+
833
+ break
834
+ case 'FMFO':
835
+ sortings.push({ name: 'iv.manufacture_date', desc: false })
836
+ sortings.push({ name: 'iv.created_at', desc: false })
837
+
838
+ break
839
+ case 'LOCATION':
840
+ const pickingProductSetting: Setting = await tx.getRepository(Setting).findOne({
841
+ where: { domain, name: 'rule-for-picking-product' }
842
+ })
843
+ if (pickingProductSetting?.value) {
844
+ for (const [key, value] of Object.entries(JSON.parse(pickingProductSetting.value))) {
845
+ sortings.push({ name: `loc.${key}`, desc: value == 'DESC' ? true : false })
846
+ }
847
+ } else {
848
+ sortings.push({ name: 'loc.name', desc: true })
849
+ }
850
+
851
+ break
852
+ //Every other case includes 'FIFO' will be applicable for this case
853
+ default:
854
+ sortings.push({ name: 'iv.created_at', desc: false })
855
+ break
856
+ }
857
+
858
+ let queryFilters = []
859
+
860
+ let queryStrings = queryFilters.reduce(
861
+ (acc, itm, idx, arr) => {
862
+ acc.values.push(itm.filters)
863
+
864
+ switch (itm?.operator) {
865
+ case 'notin':
866
+ case 'in':
867
+ acc.query.push(
868
+ `${itm.query} ${itm.operator == 'notin' ? 'NOT IN' : 'IN'} (${itm.filtes
869
+ .map((itm, idx) => {
870
+ return `$${acc.values.length + 1}`
871
+ })
872
+ .join(',')})`
873
+ )
874
+ break
875
+
876
+ default:
877
+ acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
878
+ break
879
+ }
880
+ acc.query.push(`${itm.query} ${itm?.operator ? itm.operator : '='} $${acc.values.length + 1}`)
948
881
  },
949
- orderProducts: orderInventories
950
- },
951
- context: {
952
- ...context,
953
- hasUpload: true,
954
- state: {
955
- ...context.state,
956
- customerBizplace,
957
- domain,
958
- type: 'api'
882
+ {
883
+ query: [],
884
+ values: []
959
885
  }
886
+ )
887
+
888
+ let sortQuery = sortings
889
+ .map(itm => {
890
+ return `${itm.name} ${itm.desc ? 'DESC' : 'ASC'}`
891
+ })
892
+ .join(`,`)
893
+
894
+ let updatedInventories = await tx.getRepository(Inventory).query(
895
+ `
896
+ update inventories tgt set locked_qty = coalesce(locked_qty,0) + src.reserve_qty,
897
+ locked_uom_value = coalesce(locked_uom_value,0) + src.reserve_uom_value,
898
+ updated_at = NOW(),
899
+ updater_id = $1
900
+ from (
901
+ select "id", "pallet_id","carton_id", "batch_id", "batch_id_ref", "unit", "uom", "packing_type", "packing_size", "manufacture_year",
902
+ "reserve_qty", (("uom_value"/"qty") * "reserve_qty") as "reserve_uom_value" from (
903
+ 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",
904
+ "release_qty", "release_uom_value", lock_amount,
905
+ case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end as "reserve_qty"
906
+ from (
907
+ select *,
908
+ qty - locked_qty as available_qty,
909
+ sum(qty - locked_qty - release_qty) over (order by sort_seq asc rows between unbounded preceding and current row) as lock_amount
910
+ from (
911
+ SELECT 0 as sort_seq, null as id, null as pallet_id, null as batch_id, null as batch_id_ref,
912
+ null as unit, '${orderInventory.uom}' as uom, '${orderInventory.packingType}' as packing_type, '${
913
+ orderInventory.packingSize
914
+ }' as packing_size,
915
+ null as manufacture_year, null as carton_id, 0 as uom_value, 0 as locked_uom_value, 0 as qty, 0 as locked_qty, null as created_at,
916
+ '${orderInventory.releaseQty}' as release_qty, '${orderInventory.releaseUomValue}' as release_uom_value
917
+ UNION
918
+ (
919
+ SELECT ROW_NUMBER() OVER(PARTITION BY iv.domain_id ORDER BY ${sortQuery}) as sort_seq,
920
+ iv.id, iv.pallet_id, iv.batch_id, iv.batch_id_ref,
921
+ iv.unit, iv.uom, iv.packing_type, iv.packing_size,
922
+ iv.manufacture_year, iv.carton_id, iv.uom_value,
923
+ coalesce(iv.locked_uom_value,0) as locked_uom_value, iv.qty, coalesce(iv.locked_qty,0) as locked_qty, iv.created_at,
924
+ 0 as release_qty, 0 as release_uom_value
925
+ FROM "inventories" "iv"
926
+ INNER JOIN "locations" "loc" ON "loc"."id"="iv"."location_id"
927
+ WHERE case when coalesce("iv"."locked_qty",0) < 0 then 0 else ("iv"."qty" - coalesce("iv"."locked_qty",0)) end > 0 AND
928
+ "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)
929
+ ${queryStrings.query.length > 0 ? `AND ${queryStrings.join(' AND ')}` : ''}
930
+ ORDER BY ${sortQuery}
931
+ )
932
+ ) dt1
933
+ ) dt2 where case when "lock_amount" < 0 then "available_qty" else "available_qty" - "lock_amount" end > 0
934
+ ) dt3 where sort_seq > 0
935
+ ) src where src.id = tgt.id
936
+ returning
937
+ tgt."id", tgt."qty", tgt."pallet_id", tgt."carton_id", tgt."batch_id", tgt."batch_id_ref", tgt."unit",
938
+ tgt."uom", tgt."packing_type", tgt."packing_size", tgt."manufacture_year",
939
+ tgt."locked_qty", tgt."uom_value", tgt."locked_uom_value", src."reserve_qty", src."reserve_uom_value";`,
940
+ [
941
+ user.id,
942
+ domain.id,
943
+ customerBizplace.id,
944
+ orderInventory.packingType,
945
+ orderInventory.packingSize,
946
+ orderInventory.product.id,
947
+ INVENTORY_STATUS.STORED,
948
+ LOCATION_TYPE.QUARANTINE,
949
+ LOCATION_TYPE.RESERVE,
950
+ ...queryStrings.values
951
+ ]
952
+ )
953
+
954
+ // let data = await qb.getMany()
955
+
956
+ let totalAssigned = updatedInventories[0].reduce((acc, inv) => {
957
+ return acc + inv.reserve_qty
958
+ }, 0)
959
+
960
+ if (orderInventory.releaseQty != totalAssigned) {
961
+ throw new ValidationError({
962
+ errorCode: 'INSUFFICIENT_STOCK',
963
+ message: 'insufficient stock',
964
+ detail: JSON.stringify({
965
+ code: 'insufficient stock',
966
+ sku: orderInventory.product.sku,
967
+ packingType: orderInventory.packingType,
968
+ packingSize: orderInventory.packingSize,
969
+ uom: orderInventory.uom,
970
+ qty: orderInventory.releaseQty,
971
+ uom_value: orderInventory.releaseUomValue
972
+ })
973
+ })
960
974
  }
961
- } as any)
962
- ).data
975
+
976
+ updatedInventories[0].forEach(inv => {
977
+ let oi = {
978
+ ...orderInventory,
979
+ inventory: { id: inv.id },
980
+ uom: inv.uom,
981
+ packingType: inv.packing_type,
982
+ batchId: inv.batch_id,
983
+ releaseQty: inv.reserve_qty,
984
+ releaseUomValue: inv.reserve_uom_value,
985
+ pickedQty: 0,
986
+ packedQty: 0,
987
+ type: ORDER_TYPES.RELEASE_OF_GOODS,
988
+ status: ORDER_INVENTORY_STATUS.PENDING_WORKSHEET,
989
+ domain: { id: domain.id },
990
+ bizplace: { id: customerBizplace.id }
991
+ }
992
+
993
+ assignedOrderInventories.push(oi)
994
+ })
995
+ }
996
+
997
+ orderProducts[opIdx].orderInventories = assignedOrderInventories
998
+ }
999
+
1000
+ return orderProducts
963
1001
  }