Fluent Commerce Logo
Sign In

Enable entering of Rejection Reason Code during pick confirm in Fluent Store

How-to Guide
Extend

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.

Steps

Step arrow right iconUse Case

As a Store Assistant I want to specify rejection reasons on the Pick step of Wave processing.

Step arrow right iconSolution 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
    `fulfilment`
    that contains these items.
  • The rejection reason is stored as a "Rejection Details" attribute in the
    `inventory position quantity`
    of type CORRECTION that is created.
  • Rejection reason codes are specified in the PICK_REJECTION_REASON_CODES setting.

Step arrow right iconTechnical 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:
    `AddRejectionReasonToFulfilment`
    into Order workflows
  • Add the new rule: AddRejectionReasonOnCorrection into the INVENTORY CATALOGUE workflow
  • Setting -
    `PICK_REJECTION_REASON_CODES`

Step arrow right iconCustomised 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

Step arrow right iconCustom 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:

[{
 SKU: <number>,
 Reason: <string>,
 Date/Time: <DD/MM/YYYY HH:mm>
}]


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:

{
 SKU: <number>,
 Reason: <string>,
 Date/Time: <DD/MM/YYYY HH:mm>
}

Step arrow right iconUpdate 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

Step arrow right iconAdd 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

Step arrow right iconAdd 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

Step arrow right iconSetting: 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

Step arrow right iconResult

After applying the changes from the above, The Confirm Pick screen would look like:

No alt provided


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

No alt provided


The INVENTORY_QUANTITY would also contain the select reason code.


Sergey Chebotarev

Sergey Chebotarev

Contributors:
Randy Chan