diff --git a/back/models/image.js b/back/models/image.js index 78d159940..a35018814 100644 --- a/back/models/image.js +++ b/back/models/image.js @@ -1,11 +1,51 @@ const fs = require('fs-extra'); const sharp = require('sharp'); const path = require('path'); +const readChunk = require('read-chunk'); +const imageType = require('image-type'); +const bmp = require('bmp-js'); module.exports = Self => { require('../methods/image/download')(Self); require('../methods/image/upload')(Self); + // Function extracted from jimp package (utils) + function scan(image, x, y, w, h, f) { + // round input + x = Math.round(x); + y = Math.round(y); + w = Math.round(w); + h = Math.round(h); + + for (let _y = y; _y < y + h; _y++) { + for (let _x = x; _x < x + w; _x++) { + const idx = (image.bitmap.width * _y + _x) << 2; + f.call(image, _x, _y, idx); + } + } + + return image; + } + + // Function extracted from jimp package (type-bmp) + function fromAGBR(bitmap) { + return scan({bitmap}, 0, 0, bitmap.width, bitmap.height, function( + x, + y, + index + ) { + const alpha = this.bitmap.data[index + 0]; + const blue = this.bitmap.data[index + 1]; + const green = this.bitmap.data[index + 2]; + const red = this.bitmap.data[index + 3]; + + this.bitmap.data[index + 0] = red; + this.bitmap.data[index + 1] = green; + this.bitmap.data[index + 2] = blue; + this.bitmap.data[index + 3] = bitmap.is_with_alpha ? alpha : 0xff; + }).bitmap; + } + Self.registerImage = async(collectionName, srcFilePath, fileName, entityId) => { const models = Self.app.models; const tx = await Self.beginTransaction({}); @@ -48,13 +88,31 @@ module.exports = Self => { const dstDir = path.join(collectionDir, 'full'); const dstFile = path.join(dstDir, file); + const buffer = readChunk.sync(srcFilePath, 0, 12); + const type = imageType(buffer); + + let sharpOptions; + let imgSrc = srcFilePath; + if (type.mime == 'image/bmp') { + const bmpBuffer = fs.readFileSync(srcFilePath); + const bmpData = fromAGBR(bmp.decode(bmpBuffer)); + imgSrc = bmpData.data; + sharpOptions = { + raw: { + width: bmpData.width, + height: bmpData.height, + channels: 4 + } + }; + } + const resizeOpts = { withoutEnlargement: true, fit: 'inside' }; await fs.mkdir(dstDir, {recursive: true}); - await sharp(srcFilePath, {failOnError: false}) + await sharp(imgSrc, sharpOptions) .resize(collection.maxWidth, collection.maxHeight, resizeOpts) .png() .toFile(dstFile); @@ -69,7 +127,7 @@ module.exports = Self => { }; await fs.mkdir(dstDir, {recursive: true}); - await sharp(srcFilePath, {failOnError: false}) + await sharp(imgSrc, sharpOptions) .resize(size.width, size.height, resizeOpts) .png() .toFile(dstFile); diff --git a/modules/item/back/methods/item-image-queue/downloadImages.js b/modules/item/back/methods/item-image-queue/downloadImages.js index e3412d6a8..2d2e8af91 100644 --- a/modules/item/back/methods/item-image-queue/downloadImages.js +++ b/modules/item/back/methods/item-image-queue/downloadImages.js @@ -46,13 +46,8 @@ module.exports = Self => { if (!image) return; - const srcFile = image.url.split('/').pop(); - const dotIndex = srcFile.lastIndexOf('.'); - - let fileName = srcFile.substring(0, dotIndex); - if (dotIndex == -1) - fileName = srcFile; - + const srcFile = image.url; + const fileName = srcFile.replace(/\.|\/|:|\?|\\|=|%/g, ''); const file = `${fileName}.png`; const filePath = path.join(tempPath, file); diff --git a/modules/item/back/methods/item/filter.js b/modules/item/back/methods/item/filter.js index f392dba87..eba0b0f91 100644 --- a/modules/item/back/methods/item/filter.js +++ b/modules/item/back/methods/item/filter.js @@ -44,6 +44,10 @@ module.exports = Self => { arg: 'description', type: 'String', description: 'The item description', + }, { + arg: 'stemMultiplier', + type: 'Integer', + description: 'The item multiplier', } ], returns: { @@ -80,16 +84,22 @@ module.exports = Self => { : {or: [{'i.name': {like: `%${value}%`}}, codeWhere]}; case 'id': return {'i.id': value}; - case 'description': - return {'i.description': {like: `%${value}%`}}; - case 'categoryFk': - return {'ic.id': value}; - case 'salesPersonFk': - return {'t.workerFk': value}; - case 'typeFk': - return {'i.typeFk': value}; case 'isActive': return {'i.isActive': value}; + case 'multiplier': + return {'i.stemMultiplier': value}; + case 'typeFk': + return {'i.typeFk': value}; + case 'category': + return {'ic.name': value}; + case 'salesPersonFk': + return {'it.workerFk': value}; + case 'origin': + return {'ori.code': value}; + case 'niche': + return {'ip.code': value}; + case 'intrastat': + return {'intr.description': value}; } }); filter = mergeFilters(filter, {where}); @@ -98,7 +108,8 @@ module.exports = Self => { let stmt; stmt = new ParameterizedSQL( - `SELECT i.id, + `SELECT + i.id, i.image, i.name, i.description, @@ -111,29 +122,30 @@ module.exports = Self => { i.tag10, i.value10, i.subName, i.isActive, - t.name type, - t.workerFk buyerFk, - u.name userName, - intr.description AS intrastat, i.stems, - ori.code AS origin, - ic.name AS category, i.density, i.stemMultiplier, + i.typeFk, + it.name AS typeName, + it.workerFk AS buyerFk, + u.name AS userName, + ori.code AS origin, + ic.name AS category, + intr.description AS intrastat, b.grouping, b.packing, - itn.code AS niche, @visibleCalc + ip.code AS niche, @visibleCalc FROM item i - LEFT JOIN itemType t ON t.id = i.typeFk - LEFT JOIN itemCategory ic ON ic.id = t.categoryFk - LEFT JOIN worker w ON w.id = t.workerFk + LEFT JOIN itemType it ON it.id = i.typeFk + LEFT JOIN itemCategory ic ON ic.id = it.categoryFk + LEFT JOIN worker w ON w.id = it.workerFk LEFT JOIN account.user u ON u.id = w.userFk LEFT JOIN intrastat intr ON intr.id = i.intrastatFk LEFT JOIN producer pr ON pr.id = i.producerFk LEFT JOIN origin ori ON ori.id = i.originFk - LEFT JOIN cache.last_buy lb ON lb.item_id = i.id AND lb.warehouse_id = t.warehouseFk + LEFT JOIN cache.last_buy lb ON lb.item_id = i.id AND lb.warehouse_id = it.warehouseFk LEFT JOIN vn.buy b ON b.id = lb.buy_id - LEFT JOIN itemPlacement itn ON itn.itemFk = i.id AND itn.warehouseFk = t.warehouseFk` + LEFT JOIN itemPlacement ip ON ip.itemFk = i.id AND ip.warehouseFk = it.warehouseFk` ); if (ctx.args.tags) { diff --git a/modules/item/front/basic-data/index.html b/modules/item/front/basic-data/index.html index 3f049675c..cec7a063f 100644 --- a/modules/item/front/basic-data/index.html +++ b/modules/item/front/basic-data/index.html @@ -113,6 +113,12 @@ ng-model="$ctrl.item.stems" rule> + + - - - - - Id - Grouping - Packing - Description - Stems - Size - Niche - Type - Category - Intrastat - Origin - Buyer - Density - Multiplier - Active - - - - - - - - - - - {{::item.id}} - - - {{::item.grouping | dashIfEmpty}} - {{::item.packing | dashIfEmpty}} - - {{::item.name}} - -

{{::item.subName}}

-
- - -
- {{::item.stems}} - {{::item.size}} - {{::item.niche}} - - {{::item.type}} - - - {{::item.category}} - - - {{::item.intrastat}} - - {{::item.origin}} - - - {{::item.userName}} - - - {{::item.density}} - {{::item.stemMultiplier}} - - - - - - - - - - - - -
-
-
+ + + + + Id + Grouping + Packing + Description + Stems + Size + Niche + Type + Category + Intrastat + Origin + Buyer + Density + Multiplier + Active + + + + + + + + + + + {{::item.id}} + + + {{::item.grouping | dashIfEmpty}} + {{::item.packing | dashIfEmpty}} + + {{::item.name}} + +

{{::item.subName}}

+
+ + +
+ {{::item.stems}} + {{::item.size}} + {{::item.niche}} + + {{::item.typeName}} + + + {{::item.category}} + + + {{::item.intrastat}} + + {{::item.origin}} + + + {{::item.userName}} + + + {{::item.density}} + {{::item.stemMultiplier}} + + + + + + + + + + + + +
+
+
@@ -127,4 +127,31 @@ - \ No newline at end of file + + + + + Filter by selection + + + Exclude selection + + + Remove filter + + + Remove all filters + + + \ No newline at end of file diff --git a/modules/item/front/index/index.js b/modules/item/front/index/index.js index cafa3e475..175beb88a 100644 --- a/modules/item/front/index/index.js +++ b/modules/item/front/index/index.js @@ -11,6 +11,36 @@ class Controller extends Section { }; } + exprBuilder(param, value) { + switch (param) { + case 'category': + return {'ic.name': value}; + case 'salesPersonFk': + return {'it.workerFk': value}; + case 'grouping': + return {'b.grouping': value}; + case 'packing': + return {'b.packing': value}; + case 'origin': + return {'ori.code': value}; + case 'niche': + return {'ip.code': value}; + case 'typeFk': + return {'i.typeFk': value}; + case 'intrastat': + return {'intr.description': value}; + case 'id': + case 'size': + case 'name': + case 'subname': + case 'isActive': + case 'density': + case 'stemMultiplier': + case 'stems': + return {[`i.${param}`]: value}; + } + } + onCloneAccept(itemFk) { return this.$http.post(`Items/${itemFk}/clone`) .then(res => { diff --git a/modules/item/front/summary/index.html b/modules/item/front/summary/index.html index 59ceaebbe..67d3bf3c6 100644 --- a/modules/item/front/summary/index.html +++ b/modules/item/front/summary/index.html @@ -55,6 +55,9 @@ + + { Self.remoteMethod('getAverageDays', { description: 'Returns the average days duration and the two warehouses of the travel.', @@ -8,7 +9,7 @@ module.exports = Self => { required: true }], returns: { - type: 'number', + type: 'object', root: true }, http: { @@ -18,15 +19,48 @@ module.exports = Self => { }); Self.getAverageDays = async agencyModeFk => { - const query = ` - SELECT t.id, t.warehouseInFk, t.warehouseOutFk, - (SELECT ROUND(AVG(DATEDIFF(t.landed, t.shipped ))) - FROM travel t - WHERE t.agencyFk = ? LIMIT 50) AS dayDuration - FROM travel t - WHERE t.agencyFk = ? ORDER BY t.id DESC LIMIT 1;`; + const conn = Self.dataSource.connector; + let stmts = []; + + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.travel'); + + stmt = new ParameterizedSQL(` + CREATE TEMPORARY TABLE tmp.travel ( + SELECT + t.id, + t.warehouseInFk, + t.warehouseOutFk, + t.landed, + t.shipped, + t.agencyFk + FROM travel t + WHERE t.agencyFk = ? LIMIT 50)`, [agencyModeFk]); + stmts.push(stmt); + + stmt = new ParameterizedSQL(` + SELECT + t.id, + t.warehouseInFk, + t.warehouseOutFk, + (SELECT ROUND(AVG(DATEDIFF(t.landed, t.shipped ))) + FROM tmp.travel t + WHERE t.agencyFk + ORDER BY id DESC LIMIT 50) AS dayDuration + FROM tmp.travel t + WHERE t.agencyFk + ORDER BY t.id DESC LIMIT 1`); + + const avgDaysIndex = stmts.push(stmt) - 1; + + stmts.push( + `DROP TEMPORARY TABLE + tmp.travel`); + + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await conn.executeStmt(sql); + + const [avgDays] = result[avgDaysIndex]; - const [avgDays] = await Self.rawSql(query, [agencyModeFk, agencyModeFk]); return avgDays; }; }; diff --git a/modules/travel/front/create/index.js b/modules/travel/front/create/index.js index cf0b2f382..a85917ca8 100644 --- a/modules/travel/front/create/index.js +++ b/modules/travel/front/create/index.js @@ -22,6 +22,9 @@ class Controller extends Section { agencyModeFk: this.travel.agencyModeFk }; this.$http.get(query, {params}).then(res => { + if (!res.data) + return; + const landed = new Date(value); const futureDate = landed.getDate() + res.data.dayDuration; landed.setDate(futureDate); diff --git a/modules/travel/front/create/index.spec.js b/modules/travel/front/create/index.spec.js index b59530604..e3f85d9ae 100644 --- a/modules/travel/front/create/index.spec.js +++ b/modules/travel/front/create/index.spec.js @@ -51,14 +51,29 @@ describe('Travel Component vnTravelCreate', () => { expect(controller.travel.warehouseOutFk).toBeUndefined(); }); + it(`should do nothing if there's no response data.`, () => { + controller.travel = {agencyModeFk: 4}; + const tomorrow = new Date(); + + const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`; + $httpBackend.expectGET(query).respond(undefined); + controller.onShippedChange(tomorrow); + $httpBackend.flush(); + + expect(controller.travel.warehouseInFk).toBeUndefined(); + expect(controller.travel.warehouseOutFk).toBeUndefined(); + expect(controller.travel.dayDuration).toBeUndefined(); + }); + it(`should fill the fields when it's selected a date and agency.`, () => { controller.travel = {agencyModeFk: 1}; const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setDate(tomorrow.getDate() + 9); const expectedResponse = { - dayDuration: 2, - warehouseInFk: 1, - warehouseOutFk: 2 + id: 8, + dayDuration: 9, + warehouseInFk: 5, + warehouseOutFk: 1 }; const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`; diff --git a/modules/worker/back/models/worker.json b/modules/worker/back/models/worker.json index 45eee23bf..cad38ac3b 100644 --- a/modules/worker/back/models/worker.json +++ b/modules/worker/back/models/worker.json @@ -42,6 +42,11 @@ "model": "Account", "foreignKey": "userFk" }, + "boss": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "bossFk" + }, "client": { "type": "belongsTo", "model": "Client", diff --git a/modules/worker/front/basic-data/index.html b/modules/worker/front/basic-data/index.html index a2cbbc637..a767eccc4 100644 --- a/modules/worker/front/basic-data/index.html +++ b/modules/worker/front/basic-data/index.html @@ -29,6 +29,15 @@ ng-model="$ctrl.worker.phone" rule> + +
diff --git a/modules/worker/front/locale/es.yml b/modules/worker/front/locale/es.yml index 63570ddf0..1414d089b 100644 --- a/modules/worker/front/locale/es.yml +++ b/modules/worker/front/locale/es.yml @@ -7,6 +7,7 @@ Extension: Extensión Fiscal identifier: NIF Go to client: Ir al cliente Last name: Apellidos +Boss: Jefe Log: Historial Private Branch Exchange: Centralita Role: Rol diff --git a/modules/worker/front/summary/index.html b/modules/worker/front/summary/index.html index 0a99959e4..d2a7e750b 100644 --- a/modules/worker/front/summary/index.html +++ b/modules/worker/front/summary/index.html @@ -30,6 +30,14 @@ + + + {{::worker.boss.nickname}} + + @@ -50,4 +58,7 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/modules/worker/front/summary/index.js b/modules/worker/front/summary/index.js index 6a4d87007..3cdb2c36f 100644 --- a/modules/worker/front/summary/index.js +++ b/modules/worker/front/summary/index.js @@ -11,8 +11,8 @@ class Controller extends Summary { this.$.worker = null; if (!value) return; - let query = `Workers/${value.id}`; - let filter = { + const query = `Workers/${value.id}`; + const filter = { include: [ { relation: 'user', @@ -31,13 +31,20 @@ class Controller extends Summary { } }] } - }, { + }, + { relation: 'client', scope: {fields: ['fi']} - }, { + }, + { + relation: 'boss', + scope: {fields: ['id', 'nickname']} + }, + { relation: 'sip', scope: {fields: ['extension']} - }, { + }, + { relation: 'department', scope: { include: { diff --git a/package-lock.json b/package-lock.json index 3ed1b37c9..40780aaf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6996,6 +6996,11 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" }, + "bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha1-4Fpj95amwf8l9Hcex62twUjAcjM=" + }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", @@ -10041,6 +10046,11 @@ } } }, + "file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==" + }, "filed-mimefix": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/filed-mimefix/-/filed-mimefix-0.1.3.tgz", @@ -12977,6 +12987,14 @@ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", "dev": true }, + "image-type": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz", + "integrity": "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==", + "requires": { + "file-type": "^10.10.0" + } + }, "imap": { "version": "0.8.19", "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz", @@ -21180,8 +21198,7 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "package-json": { "version": "4.0.1", @@ -22102,6 +22119,15 @@ "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==", "dev": true }, + "read-chunk": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz", + "integrity": "sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==", + "requires": { + "pify": "^4.0.1", + "with-open-file": "^0.1.6" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -26605,6 +26631,16 @@ "string-width": "^2.1.1" } }, + "with-open-file": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz", + "integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==", + "requires": { + "p-finally": "^1.0.0", + "p-try": "^2.1.0", + "pify": "^4.0.1" + } + }, "word-count": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/word-count/-/word-count-0.2.2.tgz", diff --git a/package.json b/package.json index 6e40883db..1fe209cf2 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "node": ">=12" }, "dependencies": { + "bmp-js": "^0.1.0", "compression": "^1.7.3", "fs-extra": "^5.0.0", "helmet": "^3.21.2", "i18n": "^0.8.4", + "image-type": "^4.1.0", "imap": "^0.8.19", "ldapjs": "^2.2.0", "loopback": "^3.26.0", @@ -30,6 +32,7 @@ "node-ssh": "^11.0.0", "object-diff": "0.0.4", "object.pick": "^1.3.0", + "read-chunk": "^3.2.0", "request": "^2.88.0", "request-promise-native": "^1.0.8", "require-yaml": "0.0.1",