@softwear/latestcollectioncore 1.0.197 → 1.0.199
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.d.ts +0 -4
- package/dist/articleStatus.js +24 -13
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/articleStatus.ts +26 -13
- package/src/types.ts +1 -0
- package/test/articleStatus.spec.js +78 -0
package/dist/articleStatus.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { TransactionI, RaGI, HrTimeframeI, TimeGranularityE, SkuI } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* The propFunction for FiltreX needs some data that can't be passed as parameters. That's why the 'globalsRelations' variable is needed.
|
|
4
|
-
* TODO: Functional: Experiment with curried functions
|
|
5
|
-
*/
|
|
6
2
|
declare function propFunction(propertyName: string, getPropertyName: Function, obj: Record<string, any>): any;
|
|
7
3
|
declare function whichShardsToProcess(dateRange: any, granularity: 'year' | 'month'): any[];
|
|
8
4
|
/**
|
package/dist/articleStatus.js
CHANGED
|
@@ -29,16 +29,20 @@ let globalNrAggegationExpressions = 0; // tracks the # aggregations rows so we c
|
|
|
29
29
|
* The propFunction for FiltreX needs some data that can't be passed as parameters. That's why the 'globalsRelations' variable is needed.
|
|
30
30
|
* TODO: Functional: Experiment with curried functions
|
|
31
31
|
*/
|
|
32
|
+
function resolvePath(obj, path) {
|
|
33
|
+
return path.reduce((o, key) => o === null || o === void 0 ? void 0 : o[key], obj);
|
|
34
|
+
}
|
|
32
35
|
function propFunction(propertyName, getPropertyName, obj) {
|
|
33
36
|
var _a, _b;
|
|
34
37
|
const positionOfDot = propertyName.indexOf('.');
|
|
35
38
|
if (positionOfDot < 0)
|
|
36
39
|
return obj[propertyName];
|
|
37
40
|
const table = propertyName.substring(0, positionOfDot);
|
|
38
|
-
const
|
|
41
|
+
const path = propertyName.substring(positionOfDot + 1).split('.');
|
|
39
42
|
if (table == 'transaction')
|
|
40
|
-
return obj
|
|
41
|
-
|
|
43
|
+
return resolvePath(obj, path);
|
|
44
|
+
const joinedRecord = (_a = globalRelations[table]) === null || _a === void 0 ? void 0 : _a[obj[globalJoinableTables[table]]];
|
|
45
|
+
return (_b = resolvePath(joinedRecord, path)) !== null && _b !== void 0 ? _b : '~ ?';
|
|
42
46
|
}
|
|
43
47
|
const lower = (s) => s.toLowerCase();
|
|
44
48
|
const upper = (s) => s.toUpperCase();
|
|
@@ -155,10 +159,14 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
|
|
|
155
159
|
.filter((join) => filterExpression.includes(join[0] + '.'))
|
|
156
160
|
.map((join) => join[1]);
|
|
157
161
|
}
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
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]);
|
|
162
170
|
const aggregationTrieRoot = {};
|
|
163
171
|
return function (transaction) {
|
|
164
172
|
if (filter) {
|
|
@@ -171,11 +179,6 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
|
|
|
171
179
|
return;
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
|
-
const aggregateKeyLeaf = getTrieLeaf(aggregationTrieRoot, transaction, aggregationGranularity);
|
|
175
|
-
if (!aggregateKeyLeaf.aggregateKey) {
|
|
176
|
-
aggregateKeyLeaf.aggregateKey = aggregationEvaluator(transaction);
|
|
177
|
-
}
|
|
178
|
-
const aggregateKey = aggregateKeyLeaf.aggregateKey;
|
|
179
182
|
// Prepare joined values for derived fields
|
|
180
183
|
if (transaction.type == types_1.transactionTypeE.SALE) {
|
|
181
184
|
const sku = globalRelations['sku'][transaction.ean];
|
|
@@ -190,8 +193,16 @@ function aggregator({ beginTimestamp, endTimestamp, filterExpression, aggregatio
|
|
|
190
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]);
|
|
191
194
|
v[AMOUNT_SOLD_DISCOUNT] = (0, index_1.round2)(v[AMOUNT_SOLD_MAX] - v[AMOUNT_SOLD_EXCL]);
|
|
192
195
|
}
|
|
193
|
-
if (totals)
|
|
196
|
+
if (totals || totalsOnly)
|
|
194
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;
|
|
195
206
|
// Single subtotals are subtotals for first grouper level
|
|
196
207
|
// Example: for 4 groupers we have rows like
|
|
197
208
|
// g0 g1 g2 g3 <- normal row with no subtotal
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
package/src/articleStatus.ts
CHANGED
|
@@ -97,13 +97,18 @@ let globalNrAggegationExpressions = 0 // tracks the # aggregations rows so we ca
|
|
|
97
97
|
* The propFunction for FiltreX needs some data that can't be passed as parameters. That's why the 'globalsRelations' variable is needed.
|
|
98
98
|
* TODO: Functional: Experiment with curried functions
|
|
99
99
|
*/
|
|
100
|
+
function resolvePath(obj: Record<string, any> | undefined, path: string[]): any {
|
|
101
|
+
return path.reduce((o, key) => o?.[key], obj)
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
function propFunction(propertyName: string, getPropertyName: Function, obj: Record<string, any>): any {
|
|
101
105
|
const positionOfDot = propertyName.indexOf('.')
|
|
102
106
|
if (positionOfDot < 0) return obj[propertyName]
|
|
103
107
|
const table = propertyName.substring(0, positionOfDot)
|
|
104
|
-
const
|
|
105
|
-
if (table == 'transaction') return obj
|
|
106
|
-
|
|
108
|
+
const path = propertyName.substring(positionOfDot + 1).split('.')
|
|
109
|
+
if (table == 'transaction') return resolvePath(obj, path)
|
|
110
|
+
const joinedRecord = globalRelations[table]?.[obj[globalJoinableTables[table]]]
|
|
111
|
+
return resolvePath(joinedRecord, path) ?? '~ ?'
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const lower = (s: string) => s.toLowerCase()
|
|
@@ -237,10 +242,14 @@ function aggregator({
|
|
|
237
242
|
.filter((join) => filterExpression.includes(join[0] + '.'))
|
|
238
243
|
.map((join) => join[1])
|
|
239
244
|
}
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
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])
|
|
244
253
|
const aggregationTrieRoot: AggregateKeyTrieNode = {}
|
|
245
254
|
|
|
246
255
|
return function (transaction: TransactionI): void {
|
|
@@ -252,11 +261,6 @@ function aggregator({
|
|
|
252
261
|
if (filterLeaf.filterResult === -1) return
|
|
253
262
|
}
|
|
254
263
|
}
|
|
255
|
-
const aggregateKeyLeaf = getTrieLeaf<AggregateKeyTrieNode>(aggregationTrieRoot, transaction, aggregationGranularity)
|
|
256
|
-
if (!aggregateKeyLeaf.aggregateKey) {
|
|
257
|
-
aggregateKeyLeaf.aggregateKey = aggregationEvaluator(transaction) as string
|
|
258
|
-
}
|
|
259
|
-
const aggregateKey = aggregateKeyLeaf.aggregateKey as string
|
|
260
264
|
// Prepare joined values for derived fields
|
|
261
265
|
if (transaction.type == transactionTypeE.SALE) {
|
|
262
266
|
const sku = globalRelations['sku'][transaction.ean]
|
|
@@ -271,7 +275,16 @@ function aggregator({
|
|
|
271
275
|
v[AMOUNT_SOLD_DISCOUNT] = round2(v[AMOUNT_SOLD_MAX] - v[AMOUNT_SOLD_EXCL])
|
|
272
276
|
}
|
|
273
277
|
|
|
274
|
-
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
|
|
275
288
|
|
|
276
289
|
// Single subtotals are subtotals for first grouper level
|
|
277
290
|
// Example: for 4 groupers we have rows like
|
package/src/types.ts
CHANGED
|
@@ -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(
|
|
@@ -271,6 +294,61 @@ describe('articleStatus', () => {
|
|
|
271
294
|
expect(result[2].vector[articleStatus.MARGIN]).toBe(50)
|
|
272
295
|
})
|
|
273
296
|
|
|
297
|
+
it('should resolve nested property paths like customer.invoiceAddress.countrycode when grouping', () => {
|
|
298
|
+
const CUSTOMERS = {
|
|
299
|
+
'*zBXU': { invoiceAddress: { countrycode: 'NL' } },
|
|
300
|
+
}
|
|
301
|
+
const rawAggregations = {}
|
|
302
|
+
articleStatus.runQuery(
|
|
303
|
+
{ beginTimeStamp: 1569316408000, endTimeStamp: 1569316409000 + msYear },
|
|
304
|
+
rawAggregations,
|
|
305
|
+
true,
|
|
306
|
+
TRANSACTIONS,
|
|
307
|
+
['customer.invoiceAddress.countrycode'],
|
|
308
|
+
SKUS,
|
|
309
|
+
{},
|
|
310
|
+
CUSTOMERS,
|
|
311
|
+
false,
|
|
312
|
+
'',
|
|
313
|
+
false
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
// Sales (custnr '*zBXU') resolve the nested path to 'NL',
|
|
317
|
+
// beginStock transactions have no custnr and fall back to '~ ?'
|
|
318
|
+
expect(Object.keys(rawAggregations)).toEqual(['** TOTAL **', '~ ?', 'NL'])
|
|
319
|
+
expect(rawAggregations['NL'][articleStatus.QTY_SOLD]).toBe(2)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('should resolve nested property paths in filter expressions', () => {
|
|
323
|
+
const CUSTOMERS = {
|
|
324
|
+
'*zBXU': { invoiceAddress: { countrycode: 'NL' } },
|
|
325
|
+
}
|
|
326
|
+
const run = (countrycode) => {
|
|
327
|
+
const rawAggregations = {}
|
|
328
|
+
articleStatus.runQuery(
|
|
329
|
+
{ beginTimeStamp: 1569316408000, endTimeStamp: 1569316409000 + msYear },
|
|
330
|
+
rawAggregations,
|
|
331
|
+
false,
|
|
332
|
+
TRANSACTIONS,
|
|
333
|
+
['transaction.ean'],
|
|
334
|
+
SKUS,
|
|
335
|
+
{},
|
|
336
|
+
CUSTOMERS,
|
|
337
|
+
false,
|
|
338
|
+
'customer.invoiceAddress.countrycode == "' + countrycode + '"',
|
|
339
|
+
false
|
|
340
|
+
)
|
|
341
|
+
return rawAggregations
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const matching = run('NL')
|
|
345
|
+
expect(Object.keys(matching)).toEqual(['** TOTAL **', '000558000143', '8700558000143'])
|
|
346
|
+
expect(matching['000558000143'][articleStatus.QTY_SOLD]).toBe(1)
|
|
347
|
+
|
|
348
|
+
const nonMatching = run('DE')
|
|
349
|
+
expect(Object.keys(nonMatching)).toEqual([])
|
|
350
|
+
})
|
|
351
|
+
|
|
274
352
|
it('should calculate shelfStock correctly with type 10 (beginShelfStock) and type 94 (picklist)', () => {
|
|
275
353
|
// Test scenario:
|
|
276
354
|
// - beginStock (type 0): 100 units
|