@softwear/latestcollectioncore 1.0.198 → 1.0.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/articleStatus.js +66 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/parseScannedBarcode.d.ts +6 -0
- package/dist/parseScannedBarcode.js +22 -0
- package/package.json +1 -1
- package/src/articleStatus.ts +65 -10
- package/src/index.ts +1 -0
- package/src/parseScannedBarcode.ts +14 -0
- package/test/articleStatus.spec.js +57 -1
- package/test/parseScannedBarcode.spec.js +30 -0
package/dist/articleStatus.js
CHANGED
|
@@ -159,10 +159,14 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
|
|
|
159
159
|
.filter((join) => filterExpression.includes(join[0] + '.'))
|
|
160
160
|
.map((join) => join[1]);
|
|
161
161
|
}
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
// Empty aggregation expression = no groupers → only the totals row
|
|
163
|
+
const totalsOnly = !aggregationExpression;
|
|
164
|
+
const aggregationEvaluator = totalsOnly ? undefined : buildAggregationEvaluator(aggregationExpression);
|
|
165
|
+
const aggregationGranularity = totalsOnly
|
|
166
|
+
? []
|
|
167
|
+
: Object.entries(globalJoinableTables)
|
|
168
|
+
.filter((join) => aggregationExpression.includes(join[0] + '.'))
|
|
169
|
+
.map((join) => join[1]);
|
|
166
170
|
const aggregationTrieRoot = {};
|
|
167
171
|
return function (transaction) {
|
|
168
172
|
if (filter) {
|
|
@@ -175,11 +179,6 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
|
|
|
175
179
|
return;
|
|
176
180
|
}
|
|
177
181
|
}
|
|
178
|
-
const aggregateKeyLeaf = getTrieLeaf(aggregationTrieRoot, transaction, aggregationGranularity);
|
|
179
|
-
if (!aggregateKeyLeaf.aggregateKey) {
|
|
180
|
-
aggregateKeyLeaf.aggregateKey = aggregationEvaluator(transaction);
|
|
181
|
-
}
|
|
182
|
-
const aggregateKey = aggregateKeyLeaf.aggregateKey;
|
|
183
182
|
// Prepare joined values for derived fields
|
|
184
183
|
if (transaction.type == types_1.transactionTypeE.SALE) {
|
|
185
184
|
const sku = globalRelations['sku'][transaction.ean];
|
|
@@ -194,8 +193,16 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
|
|
|
194
193
|
v[AMOUNT_SOLD_MAX] = (0, index_1.round2)((((sku === null || sku === void 0 ? void 0 : sku.price) || 0) / ((100 + (transaction.vat || (sku === null || sku === void 0 ? void 0 : sku.vat) || 21)) / 100)) * v[QTY_SOLD]);
|
|
195
194
|
v[AMOUNT_SOLD_DISCOUNT] = (0, index_1.round2)(v[AMOUNT_SOLD_MAX] - v[AMOUNT_SOLD_EXCL]);
|
|
196
195
|
}
|
|
197
|
-
if (totals)
|
|
196
|
+
if (totals || totalsOnly)
|
|
198
197
|
addTransactionToAggregations(beginTimestamp, endTimestamp, '** TOTAL **', rawAggregations, transaction, maxRows);
|
|
198
|
+
// No detail/subtotal rows when there are no groupers
|
|
199
|
+
if (totalsOnly)
|
|
200
|
+
return;
|
|
201
|
+
const aggregateKeyLeaf = getTrieLeaf(aggregationTrieRoot, transaction, aggregationGranularity);
|
|
202
|
+
if (!aggregateKeyLeaf.aggregateKey) {
|
|
203
|
+
aggregateKeyLeaf.aggregateKey = aggregationEvaluator(transaction);
|
|
204
|
+
}
|
|
205
|
+
const aggregateKey = aggregateKeyLeaf.aggregateKey;
|
|
199
206
|
// Single subtotals are subtotals for first grouper level
|
|
200
207
|
// Example: for 4 groupers we have rows like
|
|
201
208
|
// g0 g1 g2 g3 <- normal row with no subtotal
|
|
@@ -357,6 +364,51 @@ function runQuery(timeframe, rawAggregations, beginStock, transactions, selected
|
|
|
357
364
|
}
|
|
358
365
|
return rawAggregations;
|
|
359
366
|
}
|
|
367
|
+
/** True when the bound casts to an ISO instant. A calendar day or a stray "T" does not. */
|
|
368
|
+
function hasClockTime(raw) {
|
|
369
|
+
const value = String(raw).trim();
|
|
370
|
+
const cast = new Date(value);
|
|
371
|
+
if (Number.isNaN(cast.getTime()))
|
|
372
|
+
return false;
|
|
373
|
+
const iso = cast.toISOString();
|
|
374
|
+
// yyyy-MM-dd casts to UTC midnight, but the string is only that date prefix.
|
|
375
|
+
if (iso.slice(0, 10) === value)
|
|
376
|
+
return false;
|
|
377
|
+
// The time designator is the character after yyyy-MM-dd, not a stray "T".
|
|
378
|
+
return value.length > 10 && value[10] === 'T';
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Exact begin/end for two ISO instants. The end bound is that instant, not end-of-day.
|
|
382
|
+
*/
|
|
383
|
+
function getDateRangeFromIsoInstant(inputDates) {
|
|
384
|
+
const dates = inputDates.slice();
|
|
385
|
+
dates.sort((a, b) => {
|
|
386
|
+
return ('' + a).localeCompare(b);
|
|
387
|
+
});
|
|
388
|
+
const fromDate = new Date(Date.parse(dates[0]));
|
|
389
|
+
const toDate = new Date(Date.parse(dates[1]));
|
|
390
|
+
const beginTimeStamp = fromDate.getTime();
|
|
391
|
+
const endTimeStamp = toDate.getTime();
|
|
392
|
+
const timeFrame = endTimeStamp - beginTimeStamp;
|
|
393
|
+
return {
|
|
394
|
+
beginTimeStamp,
|
|
395
|
+
endTimeStamp,
|
|
396
|
+
timeFrame,
|
|
397
|
+
utcFromYear: fromDate.getUTCFullYear(),
|
|
398
|
+
utcToYear: toDate.getUTCFullYear(),
|
|
399
|
+
utcFromMonth: fromDate.getUTCMonth() + 1,
|
|
400
|
+
utcToMonth: toDate.getUTCMonth() + 1,
|
|
401
|
+
fromYear: parseInt((0, date_fns_1.format)(fromDate, 'yyyy')),
|
|
402
|
+
toYear: parseInt((0, date_fns_1.format)(toDate, 'yyyy')),
|
|
403
|
+
fromMonth: parseInt((0, date_fns_1.format)(fromDate, 'MM')),
|
|
404
|
+
toMonth: parseInt((0, date_fns_1.format)(toDate, 'MM')),
|
|
405
|
+
hrYear: (0, date_fns_1.format)(fromDate, 'yyyy'),
|
|
406
|
+
hrQuarter: (0, date_fns_1.format)(fromDate, 'yyyy-QQQ'),
|
|
407
|
+
hrMonth: (0, date_fns_1.format)(fromDate, 'yyyy-MM'),
|
|
408
|
+
hrWeek: (0, date_fns_1.format)(fromDate, 'RRRR-II'),
|
|
409
|
+
hrDay: (0, date_fns_1.format)(fromDate, 'yyyy-MM-dd'),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
360
412
|
/**
|
|
361
413
|
* @param inputDates array of two YYYY-MM-DD strings
|
|
362
414
|
*/
|
|
@@ -380,6 +432,8 @@ const getDateRangeFromPicker = function (inputDates) {
|
|
|
380
432
|
hrWeek: '',
|
|
381
433
|
hrDay: '',
|
|
382
434
|
};
|
|
435
|
+
if (hasClockTime(inputDates[0]) || hasClockTime(inputDates[1]))
|
|
436
|
+
return getDateRangeFromIsoInstant(inputDates);
|
|
383
437
|
const dates = inputDates.slice();
|
|
384
438
|
dates.sort((a, b) => {
|
|
385
439
|
return ('' + a).localeCompare(b);
|
|
@@ -432,6 +486,8 @@ const getDateRangeFromDatePair = function (inputDates) {
|
|
|
432
486
|
hrWeek: '',
|
|
433
487
|
hrDay: '',
|
|
434
488
|
};
|
|
489
|
+
if (hasClockTime(inputDates[0]) || hasClockTime(inputDates[1]))
|
|
490
|
+
return getDateRangeFromIsoInstant(inputDates);
|
|
435
491
|
const dates = inputDates.slice();
|
|
436
492
|
dates.sort((a, b) => {
|
|
437
493
|
return ('' + a).localeCompare(b);
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { default as hashBrand } from './hashBrand';
|
|
|
13
13
|
export { default as hasOnlyDigits } from './hasOnlyDigits';
|
|
14
14
|
export { default as imageBinder } from './imageBinder';
|
|
15
15
|
export { default as isean13 } from './isean13';
|
|
16
|
+
export { default as parseScannedBarcode } from './parseScannedBarcode';
|
|
16
17
|
export { default as pivotTable } from './pivotTable';
|
|
17
18
|
export { default as projectObject } from './projectObject';
|
|
18
19
|
export { default as reports, genPDF } from './reports';
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.isAxiosError = exports.exponentialDelay = exports.axiosRetry = exports.createAxiosInstance = exports.lcAxios = exports.shouldSuppressZeroField = exports.resolveLayoutFieldText = exports.isZeroFieldValue = exports.isCurrencyFieldFormat = exports.transaction = exports.sizeToMap = exports.round2 = exports.genPDF = exports.reports = exports.projectObject = exports.pivotTable = exports.isean13 = exports.imageBinder = exports.hasOnlyDigits = exports.hashBrand = exports.getPreferedPropertyMappings = exports.getBrandName = exports.findMetaBrandSetting = exports.findSkuByBarcode = exports.ensureImpliedProperties = exports.ensureArray = exports.edifact = exports.ean13 = exports.deepCopy = exports.buildPropertyMappingFn = exports.articleStatus = void 0;
|
|
20
|
+
exports.isAxiosError = exports.exponentialDelay = exports.axiosRetry = exports.createAxiosInstance = exports.lcAxios = exports.shouldSuppressZeroField = exports.resolveLayoutFieldText = exports.isZeroFieldValue = exports.isCurrencyFieldFormat = exports.transaction = exports.sizeToMap = exports.round2 = exports.genPDF = exports.reports = exports.projectObject = exports.pivotTable = exports.parseScannedBarcode = exports.isean13 = exports.imageBinder = exports.hasOnlyDigits = exports.hashBrand = exports.getPreferedPropertyMappings = exports.getBrandName = exports.findMetaBrandSetting = exports.findSkuByBarcode = exports.ensureImpliedProperties = exports.ensureArray = exports.edifact = exports.ean13 = exports.deepCopy = exports.buildPropertyMappingFn = exports.articleStatus = void 0;
|
|
21
21
|
var articleStatus_1 = require("./articleStatus");
|
|
22
22
|
Object.defineProperty(exports, "articleStatus", { enumerable: true, get: function () { return __importDefault(articleStatus_1).default; } });
|
|
23
23
|
var buildPropertyMappingFn_1 = require("./buildPropertyMappingFn");
|
|
@@ -48,6 +48,8 @@ var imageBinder_1 = require("./imageBinder");
|
|
|
48
48
|
Object.defineProperty(exports, "imageBinder", { enumerable: true, get: function () { return __importDefault(imageBinder_1).default; } });
|
|
49
49
|
var isean13_1 = require("./isean13");
|
|
50
50
|
Object.defineProperty(exports, "isean13", { enumerable: true, get: function () { return __importDefault(isean13_1).default; } });
|
|
51
|
+
var parseScannedBarcode_1 = require("./parseScannedBarcode");
|
|
52
|
+
Object.defineProperty(exports, "parseScannedBarcode", { enumerable: true, get: function () { return __importDefault(parseScannedBarcode_1).default; } });
|
|
51
53
|
var pivotTable_1 = require("./pivotTable");
|
|
52
54
|
Object.defineProperty(exports, "pivotTable", { enumerable: true, get: function () { return __importDefault(pivotTable_1).default; } });
|
|
53
55
|
var projectObject_1 = require("./projectObject");
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate for scanner / typed barcode input.
|
|
3
|
+
* Strips a leading `#` (scanner prefix), then accepts only 12- or 13-digit strings.
|
|
4
|
+
* Does not validate the EAN check digit (isean13) and does not pad a leading 0 (findSkuByBarcode).
|
|
5
|
+
*/
|
|
6
|
+
export default function parseScannedBarcode(raw: unknown): string | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const hasOnlyDigits_1 = __importDefault(require("./hasOnlyDigits"));
|
|
7
|
+
/**
|
|
8
|
+
* Gate for scanner / typed barcode input.
|
|
9
|
+
* Strips a leading `#` (scanner prefix), then accepts only 12- or 13-digit strings.
|
|
10
|
+
* Does not validate the EAN check digit (isean13) and does not pad a leading 0 (findSkuByBarcode).
|
|
11
|
+
*/
|
|
12
|
+
function parseScannedBarcode(raw) {
|
|
13
|
+
if (typeof raw !== 'string')
|
|
14
|
+
return null;
|
|
15
|
+
const scan = raw.startsWith('#') ? raw.substring(1) : raw;
|
|
16
|
+
if (scan.length !== 12 && scan.length !== 13)
|
|
17
|
+
return null;
|
|
18
|
+
if (!(0, hasOnlyDigits_1.default)(scan))
|
|
19
|
+
return null;
|
|
20
|
+
return scan;
|
|
21
|
+
}
|
|
22
|
+
exports.default = parseScannedBarcode;
|
package/package.json
CHANGED
package/src/articleStatus.ts
CHANGED
|
@@ -242,10 +242,14 @@ function aggregator({
|
|
|
242
242
|
.filter((join) => filterExpression.includes(join[0] + '.'))
|
|
243
243
|
.map((join) => join[1])
|
|
244
244
|
}
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
// Empty aggregation expression = no groupers → only the totals row
|
|
246
|
+
const totalsOnly = !aggregationExpression
|
|
247
|
+
const aggregationEvaluator = totalsOnly ? undefined : buildAggregationEvaluator(aggregationExpression)
|
|
248
|
+
const aggregationGranularity = totalsOnly
|
|
249
|
+
? []
|
|
250
|
+
: Object.entries(globalJoinableTables)
|
|
251
|
+
.filter((join) => aggregationExpression.includes(join[0] + '.'))
|
|
252
|
+
.map((join) => join[1])
|
|
249
253
|
const aggregationTrieRoot: AggregateKeyTrieNode = {}
|
|
250
254
|
|
|
251
255
|
return function (transaction: TransactionI): void {
|
|
@@ -257,11 +261,6 @@ function aggregator({
|
|
|
257
261
|
if (filterLeaf.filterResult === -1) return
|
|
258
262
|
}
|
|
259
263
|
}
|
|
260
|
-
const aggregateKeyLeaf = getTrieLeaf<AggregateKeyTrieNode>(aggregationTrieRoot, transaction, aggregationGranularity)
|
|
261
|
-
if (!aggregateKeyLeaf.aggregateKey) {
|
|
262
|
-
aggregateKeyLeaf.aggregateKey = aggregationEvaluator(transaction) as string
|
|
263
|
-
}
|
|
264
|
-
const aggregateKey = aggregateKeyLeaf.aggregateKey as string
|
|
265
264
|
// Prepare joined values for derived fields
|
|
266
265
|
if (transaction.type == transactionTypeE.SALE) {
|
|
267
266
|
const sku = globalRelations['sku'][transaction.ean]
|
|
@@ -276,7 +275,16 @@ function aggregator({
|
|
|
276
275
|
v[AMOUNT_SOLD_DISCOUNT] = round2(v[AMOUNT_SOLD_MAX] - v[AMOUNT_SOLD_EXCL])
|
|
277
276
|
}
|
|
278
277
|
|
|
279
|
-
if (totals) addTransactionToAggregations(beginTimestamp, endTimestamp, '** TOTAL **', rawAggregations, transaction, maxRows)
|
|
278
|
+
if (totals || totalsOnly) addTransactionToAggregations(beginTimestamp, endTimestamp, '** TOTAL **', rawAggregations, transaction, maxRows)
|
|
279
|
+
|
|
280
|
+
// No detail/subtotal rows when there are no groupers
|
|
281
|
+
if (totalsOnly) return
|
|
282
|
+
|
|
283
|
+
const aggregateKeyLeaf = getTrieLeaf<AggregateKeyTrieNode>(aggregationTrieRoot, transaction, aggregationGranularity)
|
|
284
|
+
if (!aggregateKeyLeaf.aggregateKey) {
|
|
285
|
+
aggregateKeyLeaf.aggregateKey = aggregationEvaluator!(transaction) as string
|
|
286
|
+
}
|
|
287
|
+
const aggregateKey = aggregateKeyLeaf.aggregateKey as string
|
|
280
288
|
|
|
281
289
|
// Single subtotals are subtotals for first grouper level
|
|
282
290
|
// Example: for 4 groupers we have rows like
|
|
@@ -449,6 +457,51 @@ function runQuery(
|
|
|
449
457
|
return rawAggregations
|
|
450
458
|
}
|
|
451
459
|
|
|
460
|
+
/** True when the bound casts to an ISO instant. A calendar day or a stray "T" does not. */
|
|
461
|
+
function hasClockTime(raw: string): boolean {
|
|
462
|
+
const value = String(raw).trim()
|
|
463
|
+
const cast = new Date(value)
|
|
464
|
+
if (Number.isNaN(cast.getTime())) return false
|
|
465
|
+
const iso = cast.toISOString()
|
|
466
|
+
// yyyy-MM-dd casts to UTC midnight, but the string is only that date prefix.
|
|
467
|
+
if (iso.slice(0, 10) === value) return false
|
|
468
|
+
// The time designator is the character after yyyy-MM-dd, not a stray "T".
|
|
469
|
+
return value.length > 10 && value[10] === 'T'
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Exact begin/end for two ISO instants. The end bound is that instant, not end-of-day.
|
|
474
|
+
*/
|
|
475
|
+
function getDateRangeFromIsoInstant(inputDates: string[]): HrTimeframeI {
|
|
476
|
+
const dates = inputDates.slice()
|
|
477
|
+
dates.sort((a, b) => {
|
|
478
|
+
return ('' + a).localeCompare(b)
|
|
479
|
+
})
|
|
480
|
+
const fromDate = new Date(Date.parse(dates[0]))
|
|
481
|
+
const toDate = new Date(Date.parse(dates[1]))
|
|
482
|
+
const beginTimeStamp = fromDate.getTime()
|
|
483
|
+
const endTimeStamp = toDate.getTime()
|
|
484
|
+
const timeFrame = endTimeStamp - beginTimeStamp
|
|
485
|
+
return {
|
|
486
|
+
beginTimeStamp,
|
|
487
|
+
endTimeStamp,
|
|
488
|
+
timeFrame,
|
|
489
|
+
utcFromYear: fromDate.getUTCFullYear(),
|
|
490
|
+
utcToYear: toDate.getUTCFullYear(),
|
|
491
|
+
utcFromMonth: fromDate.getUTCMonth() + 1,
|
|
492
|
+
utcToMonth: toDate.getUTCMonth() + 1,
|
|
493
|
+
fromYear: parseInt(format(fromDate, 'yyyy')),
|
|
494
|
+
toYear: parseInt(format(toDate, 'yyyy')),
|
|
495
|
+
fromMonth: parseInt(format(fromDate, 'MM')),
|
|
496
|
+
toMonth: parseInt(format(toDate, 'MM')),
|
|
497
|
+
hrYear: format(fromDate, 'yyyy'),
|
|
498
|
+
hrQuarter: format(fromDate, 'yyyy-QQQ'),
|
|
499
|
+
hrMonth: format(fromDate, 'yyyy-MM'),
|
|
500
|
+
hrWeek: format(fromDate, 'RRRR-II'),
|
|
501
|
+
hrDay: format(fromDate, 'yyyy-MM-dd'),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
452
505
|
/**
|
|
453
506
|
* @param inputDates array of two YYYY-MM-DD strings
|
|
454
507
|
*/
|
|
@@ -472,6 +525,7 @@ const getDateRangeFromPicker = function (inputDates: string[]): HrTimeframeI {
|
|
|
472
525
|
hrWeek: '',
|
|
473
526
|
hrDay: '',
|
|
474
527
|
}
|
|
528
|
+
if (hasClockTime(inputDates[0]) || hasClockTime(inputDates[1])) return getDateRangeFromIsoInstant(inputDates)
|
|
475
529
|
const dates = inputDates.slice()
|
|
476
530
|
dates.sort((a, b) => {
|
|
477
531
|
return ('' + a).localeCompare(b)
|
|
@@ -526,6 +580,7 @@ const getDateRangeFromDatePair = function (inputDates: string[]): HrTimeframeI {
|
|
|
526
580
|
hrWeek: '',
|
|
527
581
|
hrDay: '',
|
|
528
582
|
}
|
|
583
|
+
if (hasClockTime(inputDates[0]) || hasClockTime(inputDates[1])) return getDateRangeFromIsoInstant(inputDates)
|
|
529
584
|
const dates = inputDates.slice()
|
|
530
585
|
dates.sort((a, b) => {
|
|
531
586
|
return ('' + a).localeCompare(b)
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { default as hashBrand } from './hashBrand'
|
|
|
13
13
|
export { default as hasOnlyDigits } from './hasOnlyDigits'
|
|
14
14
|
export { default as imageBinder } from './imageBinder'
|
|
15
15
|
export { default as isean13 } from './isean13'
|
|
16
|
+
export { default as parseScannedBarcode } from './parseScannedBarcode'
|
|
16
17
|
export { default as pivotTable } from './pivotTable'
|
|
17
18
|
export { default as projectObject } from './projectObject'
|
|
18
19
|
export { default as reports, genPDF } from './reports'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import hasOnlyDigits from './hasOnlyDigits'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gate for scanner / typed barcode input.
|
|
5
|
+
* Strips a leading `#` (scanner prefix), then accepts only 12- or 13-digit strings.
|
|
6
|
+
* Does not validate the EAN check digit (isean13) and does not pad a leading 0 (findSkuByBarcode).
|
|
7
|
+
*/
|
|
8
|
+
export default function parseScannedBarcode(raw: unknown): string | null {
|
|
9
|
+
if (typeof raw !== 'string') return null
|
|
10
|
+
const scan = raw.startsWith('#') ? raw.substring(1) : raw
|
|
11
|
+
if (scan.length !== 12 && scan.length !== 13) return null
|
|
12
|
+
if (!hasOnlyDigits(scan)) return null
|
|
13
|
+
return scan
|
|
14
|
+
}
|
|
@@ -178,6 +178,29 @@ describe('articleStatus', () => {
|
|
|
178
178
|
expect(result[2].vector[articleStatus.VALUE_AVG_STOCK]).toBe(84.15000000000002)
|
|
179
179
|
})
|
|
180
180
|
|
|
181
|
+
it('should calculate only the totals row when no groupers are selected', () => {
|
|
182
|
+
const rawAggregations = {}
|
|
183
|
+
articleStatus.runQuery(
|
|
184
|
+
{ beginTimeStamp: 1569316408000, endTimeStamp: 1569316409000 + msYear },
|
|
185
|
+
rawAggregations,
|
|
186
|
+
true,
|
|
187
|
+
TRANSACTIONS,
|
|
188
|
+
[],
|
|
189
|
+
SKUS,
|
|
190
|
+
{},
|
|
191
|
+
{},
|
|
192
|
+
false,
|
|
193
|
+
'transaction.ean=="000558000143"',
|
|
194
|
+
false
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
expect(Object.keys(rawAggregations)).toEqual(['** TOTAL **'])
|
|
198
|
+
const vector = [].slice.call(rawAggregations['** TOTAL **'])
|
|
199
|
+
articleStatus.postAgg(vector, msYear)
|
|
200
|
+
expect(vector[articleStatus.QTY_STOCK]).toBe(18)
|
|
201
|
+
expect(vector[articleStatus.QTY_END_STOCK]).toBe(17)
|
|
202
|
+
})
|
|
203
|
+
|
|
181
204
|
it('should calculate aggregations', () => {
|
|
182
205
|
const rawAggregations = {}
|
|
183
206
|
articleStatus.runQuery(
|
|
@@ -526,6 +549,7 @@ describe('articleStatus', () => {
|
|
|
526
549
|
it('#getDateRangeFromDatePair', () => {
|
|
527
550
|
const dateRange = articleStatus.getDateRangeFromDatePair(['2023-12-31T23:00:00.000Z', '2024-12-31T23:00:00.000Z'])
|
|
528
551
|
expect(dateRange.beginTimeStamp).toBe(1704063600000)
|
|
552
|
+
expect(dateRange.endTimeStamp).toBe(Date.parse('2024-12-31T23:00:00.000Z'))
|
|
529
553
|
expect(dateRange.fromYear).toBe(2024)
|
|
530
554
|
expect(dateRange.fromMonth).toBe(1)
|
|
531
555
|
expect(dateRange.toYear).toBe(2025)
|
|
@@ -535,7 +559,39 @@ describe('articleStatus', () => {
|
|
|
535
559
|
expect(dateRange.hrMonth).toBe('2024-01')
|
|
536
560
|
expect(dateRange.hrWeek).toBe('2024-01')
|
|
537
561
|
expect(dateRange.hrDay).toBe('2024-01-01')
|
|
538
|
-
expect(dateRange.timeFrame).toBe(
|
|
562
|
+
expect(dateRange.timeFrame).toBe(366 * 24 * 3600 * 1000)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('#getDateRangeFromDatePair keeps clock time for since-last-run windows', () => {
|
|
566
|
+
const from = '2026-09-18T11:04:01.000Z'
|
|
567
|
+
const to = '2026-09-21T15:04:32.000Z'
|
|
568
|
+
const dateRange = articleStatus.getDateRangeFromDatePair([from, to])
|
|
569
|
+
expect(dateRange.beginTimeStamp).toBe(Date.parse(from))
|
|
570
|
+
expect(dateRange.endTimeStamp).toBe(Date.parse(to))
|
|
571
|
+
expect(dateRange.timeFrame).toBe(Date.parse(to) - Date.parse(from))
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('#getDateRangeFromDatePair date-only still includes the end day', () => {
|
|
575
|
+
const dateRange = articleStatus.getDateRangeFromDatePair(['2024-01-01', '2024-01-01'])
|
|
576
|
+
expect(dateRange.beginTimeStamp).toBe(Date.parse('2024-01-01'))
|
|
577
|
+
expect(dateRange.endTimeStamp).toBe(Date.parse('2024-01-01') + 24 * 3600 * 1000)
|
|
578
|
+
expect(dateRange.timeFrame).toBe(24 * 3600 * 1000)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('#getDateRangeFromPicker delegates ISO bounds to exact timestamps', () => {
|
|
582
|
+
const from = '2026-09-18T11:04:01.000Z'
|
|
583
|
+
const to = '2026-09-21T15:04:32.000Z'
|
|
584
|
+
const dateRange = articleStatus.getDateRangeFromPicker([from, to])
|
|
585
|
+
expect(dateRange.beginTimeStamp).toBe(Date.parse(from))
|
|
586
|
+
expect(dateRange.endTimeStamp).toBe(Date.parse(to))
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('#getDateRangeFromDatePair keeps a real ISO instant that is not the canonical spelling', () => {
|
|
590
|
+
const raw = '2024-01-01T00:00:00Z'
|
|
591
|
+
const dateRange = articleStatus.getDateRangeFromDatePair([raw, raw])
|
|
592
|
+
expect(new Date(raw).toISOString()).not.toBe(raw)
|
|
593
|
+
expect(dateRange.beginTimeStamp).toBe(new Date(raw).getTime())
|
|
594
|
+
expect(dateRange.endTimeStamp).toBe(new Date(raw).getTime())
|
|
539
595
|
})
|
|
540
596
|
|
|
541
597
|
it('should calculate ROI percentage from post-aggregated values', () => {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { parseScannedBarcode } = require('../dist/index')
|
|
2
|
+
|
|
3
|
+
describe('parseScannedBarcode', function () {
|
|
4
|
+
it('returns null for non-strings and empty input', function () {
|
|
5
|
+
expect(parseScannedBarcode(undefined)).toBe(null)
|
|
6
|
+
expect(parseScannedBarcode(null)).toBe(null)
|
|
7
|
+
expect(parseScannedBarcode(1234567890123)).toBe(null)
|
|
8
|
+
expect(parseScannedBarcode('')).toBe(null)
|
|
9
|
+
expect(parseScannedBarcode('#')).toBe(null)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('strips a leading # and accepts 12- or 13-digit scans', function () {
|
|
13
|
+
expect(parseScannedBarcode('195093100888')).toBe('195093100888')
|
|
14
|
+
expect(parseScannedBarcode('0195093100888')).toBe('0195093100888')
|
|
15
|
+
expect(parseScannedBarcode('#195093100888')).toBe('195093100888')
|
|
16
|
+
expect(parseScannedBarcode('#0195093100888')).toBe('0195093100888')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('rejects wrong length, letters, and commas', function () {
|
|
20
|
+
expect(parseScannedBarcode('12345678901')).toBe(null)
|
|
21
|
+
expect(parseScannedBarcode('12345678901234')).toBe(null)
|
|
22
|
+
expect(parseScannedBarcode('19509310088a')).toBe(null)
|
|
23
|
+
expect(parseScannedBarcode('19509310088,8')).toBe(null)
|
|
24
|
+
expect(parseScannedBarcode('12e3456789012')).toBe(null)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('does not require a valid EAN check digit', function () {
|
|
28
|
+
expect(parseScannedBarcode('1234567890129')).toBe('1234567890129')
|
|
29
|
+
})
|
|
30
|
+
})
|