@things-factory/operato-wms 4.3.795 → 4.3.797
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.
|
@@ -7,6 +7,7 @@ import '../components/worksheet-information-popup'
|
|
|
7
7
|
import './serial-number-popup'
|
|
8
8
|
import './tote-popup'
|
|
9
9
|
import './vas-list-popup'
|
|
10
|
+
import './picking-same-carton-popup'
|
|
10
11
|
|
|
11
12
|
import gql from 'graphql-tag'
|
|
12
13
|
import { css, html } from 'lit-element'
|
|
@@ -1311,14 +1312,27 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
|
|
|
1311
1312
|
const scannedCartonId = this.cartonInput.value.trim()
|
|
1312
1313
|
if (scannedCartonId) {
|
|
1313
1314
|
let candidates = (this.data?.records || []).filter(
|
|
1314
|
-
rec =>
|
|
1315
|
+
rec =>
|
|
1316
|
+
rec.cartonId === scannedCartonId &&
|
|
1317
|
+
rec.status !== 'DONE' &&
|
|
1318
|
+
rec.status !== 'MISSING' &&
|
|
1319
|
+
!rec.completed &&
|
|
1320
|
+
rec.pickedQty < rec.releaseQty
|
|
1315
1321
|
)
|
|
1316
|
-
candidates = this._filterCandidatesByInputQty(candidates)
|
|
1317
|
-
|
|
1318
1322
|
// Validate carton id: must have at least one candidate
|
|
1319
1323
|
if (!candidates || candidates.length === 0) {
|
|
1320
1324
|
this._focusOnCartonNoInput()
|
|
1321
|
-
|
|
1325
|
+
const hasFullyPickedForCarton = (this.data?.records || []).some(
|
|
1326
|
+
rec =>
|
|
1327
|
+
rec.cartonId === scannedCartonId &&
|
|
1328
|
+
rec.status !== 'DONE' &&
|
|
1329
|
+
rec.status !== 'MISSING' &&
|
|
1330
|
+
!rec.completed &&
|
|
1331
|
+
rec.pickedQty >= rec.releaseQty
|
|
1332
|
+
)
|
|
1333
|
+
throw new Error(
|
|
1334
|
+
i18next.t(hasFullyPickedForCarton ? 'text.qty_exceed_release_qty' : 'text.invalid_carton_id')
|
|
1335
|
+
)
|
|
1322
1336
|
}
|
|
1323
1337
|
|
|
1324
1338
|
if (this._enableProductScanning) {
|
|
@@ -1328,11 +1342,14 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
|
|
|
1328
1342
|
throw new Error(i18next.t('text.empty_product_barcode'))
|
|
1329
1343
|
}
|
|
1330
1344
|
|
|
1345
|
+
this._popupCancelled = false
|
|
1331
1346
|
const matchedRecord = await this._selectRecordByGtinFromCandidates(
|
|
1332
1347
|
this.productBarcodeInput.value.trim(),
|
|
1333
1348
|
candidates
|
|
1334
1349
|
)
|
|
1335
1350
|
|
|
1351
|
+
if (this._popupCancelled) return { value: false }
|
|
1352
|
+
|
|
1336
1353
|
if (!matchedRecord) {
|
|
1337
1354
|
this._focusOnProductBarcodeInput()
|
|
1338
1355
|
this.productBarcodeInput.value = ''
|
|
@@ -1345,7 +1362,7 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
|
|
|
1345
1362
|
}
|
|
1346
1363
|
} else {
|
|
1347
1364
|
// Product scanning disabled: pick a deterministic candidate
|
|
1348
|
-
const matchedRecord = this._pickBestCandidate(candidates)
|
|
1365
|
+
const matchedRecord = this._pickBestCandidate(this._filterCandidatesByInputQty(candidates))
|
|
1349
1366
|
if (
|
|
1350
1367
|
matchedRecord &&
|
|
1351
1368
|
(!this._selectedOrderInventory || this._selectedOrderInventory.name !== matchedRecord.name)
|
|
@@ -1409,14 +1426,36 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
|
|
|
1409
1426
|
if (this._pageSettings.enablePickingRandomSeq == true) {
|
|
1410
1427
|
let cartonInputValue = this.cartonInput.value
|
|
1411
1428
|
let candidates = this.data.records.filter(
|
|
1412
|
-
rec =>
|
|
1429
|
+
rec =>
|
|
1430
|
+
rec.cartonId === cartonInputValue &&
|
|
1431
|
+
rec.status !== 'DONE' &&
|
|
1432
|
+
rec.status !== 'MISSING' &&
|
|
1433
|
+
!rec.completed &&
|
|
1434
|
+
rec.pickedQty < rec.releaseQty
|
|
1413
1435
|
)
|
|
1414
|
-
candidates
|
|
1436
|
+
// If no candidates remain, check if it's because all matching items are already fully picked
|
|
1437
|
+
if (!candidates.length) {
|
|
1438
|
+
const hasFullyPickedForCarton = this.data.records.some(
|
|
1439
|
+
rec =>
|
|
1440
|
+
rec.cartonId === cartonInputValue &&
|
|
1441
|
+
rec.status !== 'DONE' &&
|
|
1442
|
+
rec.status !== 'MISSING' &&
|
|
1443
|
+
!rec.completed &&
|
|
1444
|
+
rec.pickedQty >= rec.releaseQty
|
|
1445
|
+
)
|
|
1446
|
+
if (hasFullyPickedForCarton) {
|
|
1447
|
+
this._focusOnCartonNoInput()
|
|
1448
|
+
throw new Error(i18next.t('text.qty_exceed_release_qty'))
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1415
1452
|
if (this._enableProductScanning && this.productBarcodeInput?.value) {
|
|
1453
|
+
this._popupCancelled = false
|
|
1416
1454
|
foundRecord = await this._selectRecordByGtinFromCandidates(
|
|
1417
1455
|
this.productBarcodeInput.value.trim(),
|
|
1418
1456
|
candidates
|
|
1419
1457
|
)
|
|
1458
|
+
if (this._popupCancelled) return { value: false }
|
|
1420
1459
|
}
|
|
1421
1460
|
// If product barcode is provided but no GTIN match, stop without fallback
|
|
1422
1461
|
if (this._enableProductScanning && this.productBarcodeInput?.value && !foundRecord) {
|
|
@@ -1427,7 +1466,7 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
|
|
|
1427
1466
|
|
|
1428
1467
|
// No product barcode: deterministically choose best candidate among duplicates
|
|
1429
1468
|
if (!foundRecord && (!this._enableProductScanning || !this.productBarcodeInput?.value)) {
|
|
1430
|
-
foundRecord = this._pickBestCandidate(candidates)
|
|
1469
|
+
foundRecord = this._pickBestCandidate(this._filterCandidatesByInputQty(candidates))
|
|
1431
1470
|
}
|
|
1432
1471
|
|
|
1433
1472
|
if (foundRecord) {
|
|
@@ -2486,7 +2525,9 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
|
|
|
2486
2525
|
_filterCandidatesByInputQty(candidates) {
|
|
2487
2526
|
const typedQty = this._getTypedQty()
|
|
2488
2527
|
if (!typedQty || !Array.isArray(candidates) || !candidates.length) return candidates
|
|
2489
|
-
const matches = candidates.filter(
|
|
2528
|
+
const matches = candidates.filter(
|
|
2529
|
+
rec => (parseFloat(rec?.releaseQty) - parseFloat(rec?.pickedQty ?? 0)) === typedQty
|
|
2530
|
+
)
|
|
2490
2531
|
return matches.length ? matches : candidates
|
|
2491
2532
|
}
|
|
2492
2533
|
|
|
@@ -2521,9 +2562,6 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
|
|
|
2521
2562
|
try {
|
|
2522
2563
|
if (!gtin || !candidates?.length) return null
|
|
2523
2564
|
|
|
2524
|
-
// Apply qty-based narrowing first when duplicates exist
|
|
2525
|
-
candidates = this._filterCandidatesByInputQty(candidates)
|
|
2526
|
-
|
|
2527
2565
|
// Use checkGtin to resolve product-packing info to match candidate rows
|
|
2528
2566
|
const response = await client.query({
|
|
2529
2567
|
query: gql`
|
|
@@ -2563,16 +2601,77 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
|
|
|
2563
2601
|
itm.packingSize === rec.packingSize
|
|
2564
2602
|
)
|
|
2565
2603
|
)
|
|
2566
|
-
if (exactMatches.length) return this.
|
|
2604
|
+
if (exactMatches.length) return await this._pickBestCandidateOrShowPopup(exactMatches)
|
|
2567
2605
|
|
|
2568
2606
|
const productOnlyMatches = candidates.filter(rec => items.some(itm => itm.product?.id === rec?.product?.id))
|
|
2569
|
-
if (productOnlyMatches.length) return this.
|
|
2607
|
+
if (productOnlyMatches.length) return await this._pickBestCandidateOrShowPopup(productOnlyMatches)
|
|
2570
2608
|
|
|
2571
2609
|
return null
|
|
2572
2610
|
} catch (e) {
|
|
2573
2611
|
return null
|
|
2574
2612
|
}
|
|
2575
2613
|
}
|
|
2614
|
+
|
|
2615
|
+
_hasDuplicateSameProduct(candidates) {
|
|
2616
|
+
return this._getDuplicateSameProductRows(candidates).length > 0
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
_getDuplicateSameProductRows(candidates) {
|
|
2620
|
+
if (!Array.isArray(candidates) || candidates.length <= 1) return []
|
|
2621
|
+
|
|
2622
|
+
// Group by cartonId + SKU, return only rows that belong to a duplicate group
|
|
2623
|
+
const groups = {}
|
|
2624
|
+
candidates.forEach(c => {
|
|
2625
|
+
const key = `${c?.cartonId || ''}|${c?.product?.sku || ''}`
|
|
2626
|
+
if (!groups[key]) groups[key] = []
|
|
2627
|
+
groups[key].push(c)
|
|
2628
|
+
})
|
|
2629
|
+
|
|
2630
|
+
const duplicateRows = []
|
|
2631
|
+
for (const rows of Object.values(groups)) {
|
|
2632
|
+
if (rows.length > 1) duplicateRows.push(...rows)
|
|
2633
|
+
}
|
|
2634
|
+
return duplicateRows
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
_showPickingSameCartonPopup(candidates) {
|
|
2638
|
+
return new Promise(resolve => {
|
|
2639
|
+
openPopup(
|
|
2640
|
+
html`
|
|
2641
|
+
<picking-same-carton-popup
|
|
2642
|
+
.pickingInventories="${candidates}"
|
|
2643
|
+
@selection="${e => {
|
|
2644
|
+
resolve(e.detail)
|
|
2645
|
+
}}"
|
|
2646
|
+
></picking-same-carton-popup>
|
|
2647
|
+
`,
|
|
2648
|
+
{
|
|
2649
|
+
backdrop: true,
|
|
2650
|
+
size: 'large',
|
|
2651
|
+
title: i18next.t('title.inventory_selection')
|
|
2652
|
+
}
|
|
2653
|
+
)
|
|
2654
|
+
})
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
async _pickBestCandidateOrShowPopup(candidates) {
|
|
2658
|
+
if (!Array.isArray(candidates) || !candidates.length) return null
|
|
2659
|
+
|
|
2660
|
+
// If multiple candidates have the same cartonId + SKU, show popup for user to choose
|
|
2661
|
+
const duplicateRows = this._getDuplicateSameProductRows(candidates)
|
|
2662
|
+
if (duplicateRows.length) {
|
|
2663
|
+
const selectedRecord = await this._showPickingSameCartonPopup(duplicateRows)
|
|
2664
|
+
if (!selectedRecord) {
|
|
2665
|
+
// User cancelled the popup — signal callers to do nothing
|
|
2666
|
+
this._popupCancelled = true
|
|
2667
|
+
return null
|
|
2668
|
+
}
|
|
2669
|
+
return selectedRecord
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// Otherwise, use the existing logic to pick the best candidate
|
|
2673
|
+
return this._pickBestCandidate(candidates)
|
|
2674
|
+
}
|
|
2576
2675
|
}
|
|
2577
2676
|
|
|
2578
2677
|
window.customElements.define('picking-product', PickingProduct)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import '@things-factory/grist-ui'
|
|
2
|
+
|
|
3
|
+
import { css, html, LitElement } from 'lit-element'
|
|
4
|
+
import { connect } from 'pwa-helpers/connect-mixin'
|
|
5
|
+
|
|
6
|
+
import { SingleColumnFormStyles } from '@things-factory/form-ui'
|
|
7
|
+
import { i18next, localize } from '@things-factory/i18n-base'
|
|
8
|
+
import { store } from '@things-factory/shell'
|
|
9
|
+
import { isMobileDevice } from '@things-factory/utils'
|
|
10
|
+
|
|
11
|
+
class PickingSameCartonPopup extends connect(store)(localize(i18next)(LitElement)) {
|
|
12
|
+
static get properties() {
|
|
13
|
+
return {
|
|
14
|
+
config: Object,
|
|
15
|
+
data: Object,
|
|
16
|
+
pickingInventories: Array
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
super()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static get styles() {
|
|
25
|
+
return [
|
|
26
|
+
SingleColumnFormStyles,
|
|
27
|
+
css`
|
|
28
|
+
.button-container {
|
|
29
|
+
padding: 10px 0 12px 0;
|
|
30
|
+
text-align: center;
|
|
31
|
+
}
|
|
32
|
+
.button-container mwc-button {
|
|
33
|
+
margin: 0 5px;
|
|
34
|
+
}
|
|
35
|
+
data-grist {
|
|
36
|
+
overflow-y: hidden;
|
|
37
|
+
flex: 1;
|
|
38
|
+
}
|
|
39
|
+
.grist {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
flex: 1;
|
|
43
|
+
overflow-y: auto;
|
|
44
|
+
}
|
|
45
|
+
:host {
|
|
46
|
+
padding: 10px;
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
background-color: white;
|
|
51
|
+
}
|
|
52
|
+
.header {
|
|
53
|
+
padding: 10px;
|
|
54
|
+
font-weight: bold;
|
|
55
|
+
font-size: 14px;
|
|
56
|
+
color: var(--secondary-color);
|
|
57
|
+
border-bottom: 1px solid var(--secondary-color);
|
|
58
|
+
margin-bottom: 10px;
|
|
59
|
+
}
|
|
60
|
+
`
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get dataGrist() {
|
|
65
|
+
return this.shadowRoot.querySelector('data-grist')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
updated(changeProps) {
|
|
69
|
+
if (changeProps.has('pickingInventories')) {
|
|
70
|
+
if (this.pickingInventories && this.pickingInventories.length) {
|
|
71
|
+
this.data = {
|
|
72
|
+
records: this.pickingInventories.map(inv => ({
|
|
73
|
+
...inv,
|
|
74
|
+
locationName: inv.location?.name || ''
|
|
75
|
+
}))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
render() {
|
|
82
|
+
return html`
|
|
83
|
+
<div class="header">
|
|
84
|
+
${i18next.t('text.please_select_an_inventory')}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="grist">
|
|
88
|
+
<data-grist
|
|
89
|
+
id="grist"
|
|
90
|
+
.mode=${isMobileDevice() ? 'LIST' : 'GRID'}
|
|
91
|
+
.config=${this.config}
|
|
92
|
+
.data="${this.data}"
|
|
93
|
+
></data-grist>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="button-container">
|
|
97
|
+
<mwc-button
|
|
98
|
+
raised
|
|
99
|
+
@click="${this._selectPickingInventory.bind(this)}"
|
|
100
|
+
label="${i18next.t('button.select')}"
|
|
101
|
+
></mwc-button>
|
|
102
|
+
<mwc-button
|
|
103
|
+
@click="${() => {
|
|
104
|
+
this.dispatchEvent(new CustomEvent('selection', { detail: null }))
|
|
105
|
+
history.back()
|
|
106
|
+
}}"
|
|
107
|
+
label="${i18next.t('button.cancel')}"
|
|
108
|
+
></mwc-button>
|
|
109
|
+
</div>
|
|
110
|
+
`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
firstUpdated() {
|
|
114
|
+
this.config = {
|
|
115
|
+
pagination: { infinite: true },
|
|
116
|
+
rows: { selectable: { multiple: false }, appendable: false },
|
|
117
|
+
list: {
|
|
118
|
+
fields: [
|
|
119
|
+
'locationName',
|
|
120
|
+
'palletId',
|
|
121
|
+
'batchId',
|
|
122
|
+
'batchIdRef',
|
|
123
|
+
'expirationDate',
|
|
124
|
+
'qty',
|
|
125
|
+
'releaseQty',
|
|
126
|
+
'packingType'
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
columns: [
|
|
130
|
+
{ type: 'gutter', gutterName: 'sequence' },
|
|
131
|
+
{ type: 'gutter', gutterName: 'row-selector', multiple: false, header: { renderer: () => '' } },
|
|
132
|
+
{ name: 'name', hidden: true },
|
|
133
|
+
{
|
|
134
|
+
type: 'string',
|
|
135
|
+
name: 'locationName',
|
|
136
|
+
label: true,
|
|
137
|
+
header: i18next.t('field.location'),
|
|
138
|
+
width: 120
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'string',
|
|
142
|
+
name: 'palletId',
|
|
143
|
+
label: true,
|
|
144
|
+
header: i18next.t('field.pallet_id'),
|
|
145
|
+
width: 120
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'string',
|
|
149
|
+
name: 'batchId',
|
|
150
|
+
label: true,
|
|
151
|
+
header: i18next.t('field.batch_no'),
|
|
152
|
+
width: 100
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'string',
|
|
156
|
+
name: 'batchIdRef',
|
|
157
|
+
label: true,
|
|
158
|
+
header: i18next.t('field.lot_id'),
|
|
159
|
+
width: 100
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'date',
|
|
163
|
+
name: 'expirationDate',
|
|
164
|
+
label: true,
|
|
165
|
+
header: i18next.t('field.expiration'),
|
|
166
|
+
width: 100
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: 'float',
|
|
170
|
+
name: 'qty',
|
|
171
|
+
label: true,
|
|
172
|
+
header: i18next.t('field.available_qty'),
|
|
173
|
+
width: 80
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: 'float',
|
|
177
|
+
name: 'releaseQty',
|
|
178
|
+
label: true,
|
|
179
|
+
header: i18next.t('field.release_qty'),
|
|
180
|
+
width: 80
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
type: 'string',
|
|
184
|
+
name: 'packingType',
|
|
185
|
+
label: true,
|
|
186
|
+
header: i18next.t('field.packing_type'),
|
|
187
|
+
width: 100
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async _selectPickingInventory() {
|
|
194
|
+
try {
|
|
195
|
+
const selectedInventory = this.dataGrist.selected[0]
|
|
196
|
+
if (!selectedInventory) throw new Error(i18next.t('text.please_select_inventory'))
|
|
197
|
+
|
|
198
|
+
this.dispatchEvent(new CustomEvent('selection', { detail: selectedInventory }))
|
|
199
|
+
history.back()
|
|
200
|
+
} catch (e) {
|
|
201
|
+
this._showToast(e)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_showToast({ type, message }) {
|
|
206
|
+
document.dispatchEvent(
|
|
207
|
+
new CustomEvent('notify', {
|
|
208
|
+
detail: {
|
|
209
|
+
type,
|
|
210
|
+
message
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
window.customElements.define('picking-same-carton-popup', PickingSameCartonPopup)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/operato-wms",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.797",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"@things-factory/transport-base": "^4.3.767",
|
|
109
109
|
"@things-factory/tutorial-ui": "^4.3.767",
|
|
110
110
|
"@things-factory/warehouse-base": "^4.3.791",
|
|
111
|
-
"@things-factory/worksheet-base": "^4.3.
|
|
111
|
+
"@things-factory/worksheet-base": "^4.3.797"
|
|
112
112
|
},
|
|
113
113
|
"devDependencies": {
|
|
114
114
|
"@things-factory/builder": "^4.3.767",
|
|
@@ -117,5 +117,5 @@
|
|
|
117
117
|
"cypress-localstorage-commands": "^1.6.1",
|
|
118
118
|
"eslint-plugin-cypress": "^2.12.1"
|
|
119
119
|
},
|
|
120
|
-
"gitHead": "
|
|
120
|
+
"gitHead": "1acd354f47dfc15a5670841ddfb51e338f61d15f"
|
|
121
121
|
}
|