Enable entering of Rejection Reason Code during pick confirm in Fluent Store
Authors:
Sergey Chebotarev, Randy Chan
Changed on:
4 July 2024
Key Points
- This article gives System Integrator (SI) Partners and Businesses a high-level idea of where new features can be built on top of the OMS reference solution to support the customer's requirement within Fluent OMS.
- This idea/solution will provide a new feature where the Store User can enter the rejection reason code during the Pick Confirm Process.
- This feature allows specifying the rejection reason for fulfillment on the picking step of wave processing. The user can select the rejection reason code when the picked item list is confirmed as the first step of wave processing.
Prerequisites
Steps
Use Case
As a Store Assistant I want to specify rejection reasons on the Pick step of Wave processing.
Solution Approach
This feature allows for the specification of the rejection reason for fulfillment on the picking step of wave processing. The user can select the rejection reason code when the picked item list is confirmed as the first step of wave processing.
Rejection reason code is stored as an attribute of the fulfillment with these items and as an attribute of inventory position quantity of type CORRECTION created because of rejection. A list of available rejection reason codes is taken from settings.
- There is a functionality to set a rejection reason on the Pick step of Wave processing if the picked quantity is less than the requested quantity.
- The reason for rejection is stored as a "Rejection Details" attribute in the that contains these items.
`fulfilment`
- The rejection reason is stored as a "Rejection Details" attribute in the of type CORRECTION that is created.
`inventory position quantity`
- Rejection reason codes are specified in the PICK_REJECTION_REASON_CODES setting.
Technical Design Overview
To support the business solution design, the following technical areas need to be enhanced:
- A new custom UI component (by using Component SDK) -
`pickedItems`
- Two custom rules (by using Rule SDK):
`AddRejectionReasonToFulfilment`
`AddRejectionReasonOnCorrection`
- Update Ruleset in Location (Store / Warehouse) workflows ()
`PickConfirm`
- Add the new rule: into Order workflows
`AddRejectionReasonToFulfilment`
- Add the new rule: AddRejectionReasonOnCorrection into the INVENTORY CATALOGUE workflow
- Setting -
`PICK_REJECTION_REASON_CODES`
Customised UI Component: pickedItems
1<div>
2 <p>
3 {translateOr([
4 'fc.sf.ui.wave.pickAndPack.list.pick.confirm.subtitle',
5 'You have',
6 ])}
7 :
8 </p>
9 <div className={classes.cmSubTitleBlock}>
10 {totalPicked > 0 && (
11 <p className={classes.cmSubTitleAlign}>
12 <MdCheckCircle size={16} color={'#4CAF50'} />
13 <span className={classes.cmSubTitle}>
14 {translateOr([
15 'fc.sf.ui.wave.pickAndPack.list.pick.confirm.picked',
16 'Picked',
17 ])}{' '}
18 <b>
19 {totalPicked} {itemsText}
20 </b>
21 </span>
22 </p>
23 )}
24 {totalItems - totalPicked > 0 && (
25 <p className={classes.cmSubTitleAlign}>
26 <MdCancel size={16} color={'#D23B3B'} />
27 <span className={classes.cmSubTitle}>
28 {translateOr([
29 'fc.sf.ui.wave.pickAndPack.list.pick.confirm.rejected',
30 'Rejected',
31 ])}{' '}
32 <b>
33 {totalItems - totalPicked} {itemsText}
34 </b>
35 </span>
36 </p>
37 )}
38 </div>
39 {isMobileScreen &&
40 rows
41 .filter((row) => row.pickedQty - row.quantity != 0)
42 .map((row) => (
43 <Card square={true} key={row.product.ref} className={classes.card}>
44 <Grid item container className={classes.grid}>
45 <Fragment>
46 <Grid item xs={3} className={classes.fragment}>
47 <div className={classes.fragmentGrid}>
48 <Typography
49 variant={'h6'}
50 className={classes.fragmentGridTypography}
51 >
52 {translateOr([
53 'fc.sf.ui.wave.pickAndPack.list.pick.product',
54 'Product',
55 ])}
56 </Typography>
57 </div>
58 </Grid>
59 <Grid item xs={9}>
60 <Typography
61 variant={'body1'}
62 className={classes.fragmentContentGridTypography}
63 >
64 <span
65 dangerouslySetInnerHTML={{ __html: row.product.ref }}
66 />
67 </Typography>
68 </Grid>
69 </Fragment>
70 </Grid>
71 <Grid item container className={classes.grid}>
72 <Fragment>
73 <Grid item xs={3} className={classes.fragment}>
74 <div className={classes.fragmentGrid}>
75 <Typography
76 variant={'h6'}
77 className={classes.fragmentGridTypography}
78 >
79 {translateOr([
80 'fc.sf.ui.wave.pickAndPack.list.pick.reason',
81 'Reason',
82 ])}
83 </Typography>
84 </div>
85 </Grid>
86 <Grid item xs={9}>
87 <FormControl variant="outlined">
88 <InputLabel
89 className={classes.cmRowLabel}
90 id={'wavePickList_reason_input_' + row.product.ref}
91 >
92 Reason code
93 </InputLabel>
94 <Select
95 labelId={'wavePickList_reason_input_' + row.product.ref}
96 id={'wavePickList_reason_select_' + row.product.ref}
97 className={classes.cmRowSelect}
98 defaultValue={row.rejectReason}
99 onChange={(_event, reason: any) => {
100 handleRowChange(
101 row.product.ref,
102 rows,
103 setRows,
104 reason?.props.value,
105 );
106 }}
107 label={translateOr([
108 'fc.sf.ui.wave.pickAndPack.list.pick.reason.code',
109 'Reason code',
110 ])}
111 >
112 {rejectReasons &&
113 rejectReasons.map((reason: string, idx: number) => {
114 return (
115 <MenuItem key={`${reason}-${idx}`} value={reason}>
116 {reason}
117 </MenuItem>
118 );
119 })}
120 </Select>
121 </FormControl>
122 </Grid>
123 </Fragment>
124 </Grid>
125 <Grid item container className={classes.grid}>
126 <Fragment>
127 <Grid item xs={3} className={classes.fragment}>
128 <div className={classes.fragmentGrid}>
129 <Typography
130 variant={'h6'}
131 className={classes.fragmentGridTypography}
132 >
133 {translateOr([
134 'fc.sf.ui.wave.pickAndPack.list.pick.substitute',
135 'Substitute',
136 ])}
137 </Typography>
138 </div>
139 </Grid>
140 <Grid item xs={9}>
141 <FormControl fullWidth variant="outlined">
142 <InputLabel
143 className={classes.cmRowLabel}
144 id={'wavePickList_substitute_input_' + row.product.ref}
145 >
146 {row.substitutes
147 ? translateOr([
148 'fc.sf.ui.wave.pickAndPack.list.pick.substitute',
149 'Substitute',
150 ])
151 : translateOr([
152 'fc.sf.ui.wave.pickAndPack.list.pick.not.found',
153 'Not found',
154 ])}
155 </InputLabel>
156 <Select
157 labelId={
158 'wavePickList_substitute_input_' + row.product.ref
159 }
160 id={'wavePickList_substitute_select_' + row.product.ref}
161 className={classes.cmRowSelect}
162 onChange={(_event, substitute: any) => {
163 handleRowChange(
164 row.product.ref,
165 rows,
166 setRows,
167 row.rejectReason,
168 substitute?.props.value,
169 );
170 }}
171 defaultValue={row.substitute}
172 label={translateOr([
173 'fc.sf.ui.wave.pickAndPack.list.pick.substitute',
174 'Substitute',
175 ])}
176 disabled={!row.substitutes}
177 >
178 <MenuItem
179 value={undefined}
180 className={classes.cmEmptyMenuItem}
181 />
182 {row.substitutes?.map(
183 (substitute: VirtualPositionNode) => {
184 return (
185 <MenuItem
186 key={substitute.productRef}
187 value={substitute.productRef}
188 >
189 {substitute.productRef} ({substitute.quantity})
190 </MenuItem>
191 );
192 },
193 )}
194 </Select>
195 </FormControl>
196 </Grid>
197 </Fragment>
198 </Grid>
199 <Grid item container className={classes.grid}>
200 <Fragment>
201 <Grid item xs={3} className={classes.fragment}>
202 <div className={classes.fragmentGrid}>
203 <Typography
204 variant={'h6'}
205 className={classes.fragmentGridTypography}
206 >
207 {translateOr([
208 'fc.sf.ui.wave.pickAndPack.list.pick.qty',
209 'Qty',
210 ])}
211 </Typography>
212 </div>
213 </Grid>
214 <Grid item xs={9}>
215 <Typography
216 variant={'body1'}
217 className={classes.fragmentContentGridTypography}
218 >
219 <span
220 dangerouslySetInnerHTML={{
221 __html: translateOr(
222 [
223 'fc.sf.ui.wave.pickAndPack.list.pick.confirm.of',
224 `${
225 isNaN(row.quantity - row.pickedQty)
226 ? row.quantity
227 : row.quantity - row.pickedQty
228 } of ${row.quantity}`,
229 ],
230 {
231 count: isNaN(row.quantity - row.pickedQty)
232 ? row.quantity
233 : row.quantity - row.pickedQty,
234 total: row.quantity,
235 },
236 ),
237 }}
238 />
239 </Typography>
240 </Grid>
241 </Fragment>
242 </Grid>
243 </Card>
244 ))}
245 {!isMobileScreen && (
246 <Table>
247 {totalItems - totalPicked > 0 && (
248 <TableHead>
249 <TableRow>
250 <TableCell className={classes.cmRowProductName}>
251 {translateOr([
252 'fc.sf.ui.wave.pickAndPack.list.pick.product',
253 'Product',
254 ])}
255 </TableCell>
256 <TableCell className={classes.cmRowProductCell}>
257 {translateOr([
258 'fc.sf.ui.wave.pickAndPack.list.pick.reason',
259 'Reason',
260 ])}
261 </TableCell>
262 <TableCell className={classes.cmRowProductCell}>
263 {translateOr([
264 'fc.sf.ui.wave.pickAndPack.list.pick.substitute',
265 'Substitute',
266 ])}
267 </TableCell>
268 <TableCell className={classes.cmRowProductCell} align="center">
269 {translateOr([
270 'fc.sf.ui.wave.pickAndPack.list.pick.qty',
271 'Qty',
272 ])}
273 </TableCell>
274 </TableRow>
275 </TableHead>
276 )}
277 <TableBody>
278 {rows.map((row) => {
279 const qtyRejected = isNaN(row.quantity - row.pickedQty)
280 ? row.quantity
281 : row.quantity - row.pickedQty;
282 if (row.pickedQty - row.quantity == 0) {
283 return;
284 } else {
285 return (
286 <TableRow key={row.product.ref}>
287 <TableCell className={classes.cmRowProductName}>
288 <span> {row.product.name} </span>
289 <br />
290 <span className={classes.cmRowSkuRef}>
291 {row.product.ref}
292 </span>
293 </TableCell>
294 <TableCell className={classes.cmRowProductCell}>
295 <FormControl variant="outlined">
296 <InputLabel
297 className={classes.cmRowLabel}
298 id={'wavePickList_reason_input_' + row.product.ref}
299 >
300 Reason code
301 </InputLabel>
302 <Select
303 labelId={
304 'wavePickList_reason_input_' + row.product.ref
305 }
306 id={'wavePickList_reason_select_' + row.product.ref}
307 className={classes.cmRowSelect}
308 defaultValue={row.rejectReason}
309 onChange={(_event, reason: any) => {
310 handleRowChange(
311 row.product.ref,
312 rows,
313 setRows,
314 reason?.props.value,
315 );
316 }}
317 label={translateOr([
318 'fc.sf.ui.wave.pickAndPack.list.pick.reason.code',
319 'Reason code',
320 ])}
321 >
322 {rejectReasons &&
323 rejectReasons.map((reason: string, idx: number) => {
324 return (
325 <MenuItem
326 key={`${reason}-${idx}`}
327 value={reason}
328 >
329 {reason}
330 </MenuItem>
331 );
332 })}
333 </Select>
334 </FormControl>
335 </TableCell>
336 <TableCell className={classes.cmRowProductCell}>
337 <FormControl fullWidth variant="outlined">
338 <InputLabel
339 className={classes.cmRowLabel}
340 id={
341 'wavePickList_substitute_input_' + row.product.ref
342 }
343 >
344 {row.substitutes
345 ? translateOr([
346 'fc.sf.ui.wave.pickAndPack.list.pick.substitute',
347 'Substitute',
348 ])
349 : translateOr([
350 'fc.sf.ui.wave.pickAndPack.list.pick.not.found',
351 'Not found',
352 ])}
353 </InputLabel>
354 <Select
355 labelId={
356 'wavePickList_substitute_input_' + row.product.ref
357 }
358 id={
359 'wavePickList_substitute_select_' + row.product.ref
360 }
361 className={classes.cmRowSelect}
362 onChange={(_event, substitute: any) => {
363 handleRowChange(
364 row.product.ref,
365 rows,
366 setRows,
367 row.rejectReason,
368 substitute?.props.value,
369 );
370 }}
371 defaultValue={row.substitute}
372 label={translateOr([
373 'fc.sf.ui.wave.pickAndPack.list.pick.substitute',
374 'Substitute',
375 ])}
376 disabled={!row.substitutes}
377 >
378 <MenuItem
379 value={undefined}
380 className={classes.cmEmptyMenuItem}
381 />
382 {row.substitutes?.map(
383 (substitute: VirtualPositionNode) => {
384 return (
385 <MenuItem
386 key={substitute.productRef}
387 value={substitute.productRef}
388 >
389 {substitute.productRef} ({substitute.quantity}
390 )
391 </MenuItem>
392 );
393 },
394 )}
395 </Select>
396 </FormControl>
397 </TableCell>
398 <TableCell
399 className={classes.cmRowProductCell}
400 align="center"
401 >
402 <span className={classes.cmListContainerText}>
403 {translateOr(
404 [
405 'fc.sf.ui.wave.pickAndPack.list.pick.confirm.of',
406 `${qtyRejected} of ${row.quantity}`,
407 ],
408 {
409 count: qtyRejected,
410 total: row.quantity,
411 },
412 )}
413 </span>
414 </TableCell>
415 </TableRow>
416 );
417 }
418 })}
419 </TableBody>
420 </Table>
421 )}
422 </div>
Language: tsx
Name: WavePickList return snippet
Description:
Example of pick confirmation component source code
Custom Rules
Two custom Rules need to be added:
AddRejectionReasonToFulfilment
Property | Value |
Plugin name | <yourPluginName> |
Rule API Client | GraphQL |
Rule Info Description | Save Rejection Reason as an Attribute of Fulfilment if it is needed and available |
Supported Entities | FULFILMENT |
Input Parameters
This rule has no parameters. It collects all data from the event attribute 'pickedItems' created by the user action in the 'PickConfirm' ruleset.
New Attribute in Fulfilment
Attribute Name | Description |
RejectionDetails | Object with information about fulfillment in JSON format. Example: [{ |
AddRejectionReasonOnCorrection
Property | Value |
Plugin name | <yourPluginName> |
Rule API Client | GraphQL |
Rule Info Description | Save Rejection Reason as an Attribute of Inventory Quantity taken from the Fulfilment if Type is CORRECTION |
Supported Entities | INVENTORY_QUANTITY |
New Attribute in InventoryQuantity
Attribute Name | Description |
RejectionDetails | Object with information about INVENTORY_QUANTITY in JSON format. Example: { |
Update LOCATION (STORE / WAREHOUSE) workflow
1{
2 "name": "PickConfirm",
3 "description": "Wave has been created.",
4 "type": "WAVE",
5 "subtype": "STORE",
6 "eventType": "NORMAL",
7 "rules": [
8 {
9 "name": "{{fluent.account.id}}.{packageName}}.AllocateConfirmedItemsByFulfilmentExpiry",
10 "props": {
11 "eventName": "WavePack",
12 "excludedStatuses": [
13 "EXPIRED"
14 ]
15 }
16 },
17 {
18 "name": "{{fluent.account.id}}.core.SetState",
19 "props": {
20 "status": "PACK"
21 }
22 }
23 ],
24 "triggers": [
25 {
26 "status": "PICK"
27 }
28 ],
29 "userActions": [
30
31 {
32 "context": [
33 {
34 "label": "Confirm pick",
35 "type": "PRIMARY",
36 "modules": [
37 "servicepoint",
38 "store"
39 ],
40 "confirm": true
41 }
42 ],
43 "attributes": [
44 {
45 "name": "pickedItems",
46 "label": "Picked Items",
47 "type": "STRING",
48 "source": "",
49 "defaultValue": "",
50 "mandatory": false
51 }
52 ]
53 }
54 ]
55}
Language: json
Name: Ensure the Ruleset PickConfirm is calling custom Ui component pickedItems in the user action attribute
Description:
Example of LOCATION workflow
Add the new rule: AddRejectionReasonToFulfilment into Order workflows
1{
2 "name": "VerifyFulfilmentItems",
3 "description": "Verify Fulfilment Items",
4 "type": "FULFILMENT",
5 "eventType": "NORMAL",
6 "rules": [
7 {
8 "name": "{{fluent.account.id}}.{packageName}.AddRejectionReasonToFulfilment",
9 "props": {}
10 },
11 {
12 "name": "{{fluent.account.id}}.{packageName}.VerifyFulfilmentItems",
13 "props": {
14 "quantity": "PARTIAL",
15 "eventName": "PartiallyFulfilled"
16 }
17 },
18 {
19 "name": "{{fluent.account.id}}.{packageName}.VerifyFulfilmentItems",
20 "props": {
21 "quantity": "ALL",
22 "eventName": "AllFulfilled"
23 }
24 },
25 {
26 "name": "{{fluent.account.id}}.{packageName}.VerifyFulfilmentItems",
27 "props": {
28 "quantity": "NONE",
29 "eventName": "AllRejected"
30 }
31 }
32 ],
33 "triggers": [
34 {
35 "status": "ASSIGNED"
36 }
37 ],
38 "userActions": []
39}
Language: json
Name: Add the new rule: AddRejectionReasonToFulfilment into Order workflows
Description:
Example of ORDER Workflow
Add the new rule: AddRejectionReasonOnCorrection into INVENTORY CATALOGUE workflow
1{
2 "name": "CREATE",
3 "description": "",
4 "type": "INVENTORY_QUANTITY",
5 "eventType": "NORMAL",
6 "rules": [
7 {
8 "name": "{{fluent.account.id}}.core.SetState",
9 "props": {
10 "status": "ACTIVE"
11 }
12 },
13 {
14 "name": "{{fluent.account.id}}.core.SendEvent",
15 "props": {
16 "eventName": "NotifyInventoryPosition"
17 }
18 },
19 {
20 "name": "{{fluent.account.id}}.{packageName}.AddRejectionReasonOnCorrection",
21 "props": {}
22 }
23 ],
24 "triggers": [
25 {
26 "status": "CREATED"
27 }
28 ],
29 "userActions": []
30}
Language: json
Name: Add the new rule: AddRejectionReasonOnCorrection into INVENTORY CATALOGUE workflow
Description:
Example of INVENTORY_CATLOGUE Workflow
Setting: PICK_REJECTION_REASON_CODES
Name | PICK_REJECTION_REASON_CODES |
Value Type | JSON |
Context | RETAILER |
Context ID | <ContextID> |
JSON Value | [ "Out Of Stock", "Damaged Item", "Damaged Packaging" ] |
1mutation createPickRejectionReasonSetting($contextId: Int!) {
2 createSetting(
3 input: {
4 name: "PICK_REJECTION_REASON_CODES",
5 valueType: "JSON",
6 context:"RETAILER",
7 contextId: $contextId,
8 lobValue: [
9 "Out Of Stock",
10 "Damaged Item",
11 "Damaged Packaging"
12 ]
13 }
14 )
Language: graphqlschema
Name: PICK_REJECTION_REASON_CODES
Description:
PICK_REJECTION_REASON_CODES Setting
Result
After applying the changes from the above, The Confirm Pick screen would look like:

The Reason Code will be stored in fulfillment's attributes:

The INVENTORY_QUANTITY would also contain the select reason code.