diff --git a/CHANGELOG.md b/CHANGELOG.md index cbfc6e9e6..36347bd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - (Entradas -> Correo) Al cambiar el tipo de cambio enviará un correo a las personas designadas +- (General -> Históricos) Botón para ver el estado del registro en cada punto +- (General -> Históricos) Al filtar por registro se muestra todo el histórial desde que fue creado ### Changed +- (General -> Históricos) Los registros se muestran agrupados por usuario y entidad +- (Facturas -> Facturación global) Optimizada, generación de PDFs y notificaciones en paralelo ### Fixed -- +- (General -> Históricos) Duplicidades eliminadas +- (Facturas -> Facturación global) Solucionados fallos que paran el proceso ## [2324.01] - 2023-06-15 diff --git a/db/changes/232402/00-hotFix_travelConfig.sql b/db/changes/232402/00-hotFix_travelConfig.sql new file mode 100644 index 000000000..65450a74d --- /dev/null +++ b/db/changes/232402/00-hotFix_travelConfig.sql @@ -0,0 +1,22 @@ +CREATE TABLE `vn`.`travelConfig` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `warehouseInFk` smallint(6) unsigned NOT NULL DEFAULT 8 COMMENT 'Warehouse de origen', + `warehouseOutFk` smallint(6) unsigned NOT NULL DEFAULT 60 COMMENT 'Warehouse destino', + `agencyFk` int(11) NOT NULL DEFAULT 1378 COMMENT 'Agencia por defecto', + `companyFk` int(10) unsigned NOT NULL DEFAULT 442 COMMENT 'Compañía por defecto', + PRIMARY KEY (`id`), + KEY `travelConfig_FK` (`warehouseInFk`), + KEY `travelConfig_FK_1` (`warehouseOutFk`), + KEY `travelConfig_FK_2` (`agencyFk`), + KEY `travelConfig_FK_3` (`companyFk`), + CONSTRAINT `travelConfig_FK` FOREIGN KEY (`warehouseInFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `travelConfig_FK_1` FOREIGN KEY (`warehouseOutFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `travelConfig_FK_2` FOREIGN KEY (`agencyFk`) REFERENCES `agencyMode` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `travelConfig_FK_3` FOREIGN KEY (`companyFk`) REFERENCES `company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('Entry', 'addFromPackaging', 'WRITE', 'ALLOW', 'ROLE', 'production'), + ('Entry', 'addFromBuy', 'WRITE', 'ALLOW', 'ROLE', 'production'), + ('Supplier', 'getItemsPackaging', 'READ', 'ALLOW', 'ROLE', 'production'); diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 5c6350ba5..640a224cb 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -479,9 +479,6 @@ export default { fourthBalance: 'vn-item-diary vn-tbody > vn-tr:nth-child(4) > vn-td.balance > span', firstBalance: 'vn-item-diary vn-tbody > vn-tr:nth-child(1) > vn-td.balance' }, - itemLog: { - anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr', - }, ticketSummary: { header: 'vn-ticket-summary > vn-card > h5', state: 'vn-ticket-summary vn-label-value[label="State"] > section > span', @@ -667,15 +664,6 @@ export default { thirdRemoveRequestButton: 'vn-ticket-request-index vn-tr:nth-child(3) vn-icon[icon="delete"]', thirdRequestQuantity: 'vn-ticket-request-index vn-table vn-tr:nth-child(3) > vn-td:nth-child(6) vn-input-number', saveButton: 'vn-ticket-request-create button[type=submit]', - - }, - ticketLog: { - firstTD: 'vn-ticket-log vn-table vn-td:nth-child(1)', - logButton: 'vn-left-menu a[ui-sref="ticket.card.log"]', - user: 'vn-ticket-log vn-tbody vn-tr vn-td:nth-child(2)', - action: 'vn-ticket-log vn-tbody vn-tr vn-td:nth-child(4)', - changes: 'vn-ticket-log vn-data-viewer vn-tbody vn-tr table tr:nth-child(2) td.after', - id: 'vn-ticket-log vn-tr:nth-child(1) table tr:nth-child(1) td.before' }, ticketService: { addServiceButton: 'vn-ticket-service vn-icon-button[vn-tooltip="Add service"] > button', diff --git a/e2e/paths/02-client/07_edit_web_access.spec.js b/e2e/paths/02-client/07_edit_web_access.spec.js index 5386d12bd..3847ec7cd 100644 --- a/e2e/paths/02-client/07_edit_web_access.spec.js +++ b/e2e/paths/02-client/07_edit_web_access.spec.js @@ -5,8 +5,8 @@ const $ = { userName: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.name"]', email: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.email"]', saveButton: 'vn-client-web-access button[type=submit]', - nameValue: 'vn-client-log .change:nth-child(1) .basic-json:nth-child(2) vn-json-value', - activeValue: 'vn-client-log .change:nth-child(2) .basic-json:nth-child(1) vn-json-value' + nameValue: 'vn-client-log .changes-log:nth-child(2) .basic-json:nth-child(2) vn-json-value', + activeValue: 'vn-client-log .changes-log:nth-child(3) .basic-json:nth-child(1) vn-json-value' }; describe('Client web access path', () => { diff --git a/front/core/components/avatar/style.scss b/front/core/components/avatar/style.scss index 272930821..b3a80db55 100644 --- a/front/core/components/avatar/style.scss +++ b/front/core/components/avatar/style.scss @@ -4,8 +4,8 @@ vn-avatar { display: block; border-radius: 50%; overflow: hidden; - height: 36px; - width: 36px; + height: 38px; + width: 38px; font-size: 22px; background-color: $color-main; position: relative; diff --git a/front/salix/components/log/index.html b/front/salix/components/log/index.html index 8b33c811b..16342783d 100644 --- a/front/salix/components/log/index.html +++ b/front/salix/components/log/index.html @@ -2,8 +2,6 @@ vn-id="model" url="{{$ctrl.url}}" filter="$ctrl.filter" - link="{originFk: $ctrl.originId}" - where="{changedModel: $ctrl.changedModel, changedModelId: $ctrl.changedModelId}" data="$ctrl.logs" order="creationDate DESC, id DESC" limit="20"> @@ -17,90 +15,108 @@ -
-
- - - - -
+
+
+
+ {{::$ctrl.modelI18n}} #{{::originLog.originFk}} +
- -
-
- - {{::log.changedModelI18n}} - -
-
- {{::$ctrl.relativeDate(log.creationDate)}} - - -
+
+
+ + + + +
+
-
- #{{::log.changedModelId}} - - - {{::log.changedModelValue}} -
-
- - - - - - {{::prop.nameI18n}}: +
+
+
+ + + + {{::modelLog.modelI18n}} - , - -
-
- - {{::prop.nameI18n}}: - - - - ← - -
+ #{{::modelLog.id}} + {{::modelLog.showValue}}
- - - {{::log.description}} - - + +
+
+ {{::$ctrl.relativeDate(log.creationDate)}} +
+
+ + + + +
+
+
+ + + + + + {{::prop.nameI18n}}: + + , + +
+
+ + {{::prop.nameI18n}}: + + + + ← + +
+
+
+ + {{::log.description}} + + +
+
+
- + + + + +
+
+ {{$ctrl.instance.modelLog.modelI18n}} #{{$ctrl.instance.modelLog.id}} +
+
+
+ + {{::prop.nameI18n}}: + + +
+
+ No data +
+
+
+
+
+ diff --git a/front/salix/components/log/index.js b/front/salix/components/log/index.js index 8aea255a2..3df367cae 100644 --- a/front/salix/components/log/index.js +++ b/front/salix/components/log/index.js @@ -3,7 +3,10 @@ import Section from '../section'; import {hashToColor} from 'core/lib/string'; import './style.scss'; -const validDate = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/; +const validDate = new RegExp( + /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])/.source + + /T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/.source +); export default class Controller extends Section { constructor($element, $) { @@ -28,6 +31,20 @@ export default class Controller extends Section { select: 'visibility' }; this.filter = { + fields: [ + 'id', + 'originFk', + 'userFk', + 'action', + 'changedModel', + 'oldInstance', + 'newInstance', + 'creationDate', + 'changedModel', + 'changedModelId', + 'changedModelValue', + 'description' + ], include: [{ relation: 'user', scope: { @@ -48,6 +65,11 @@ export default class Controller extends Section { this.today.setHours(0, 0, 0, 0); } + $onInit() { + const match = this.url?.match(/(.*)Logs$/); + this.modelI18n = match && this.translateModel(match[1]); + } + $postLink() { this.resetFilter(); this.$.$watch( @@ -63,47 +85,75 @@ export default class Controller extends Section { set logs(value) { this._logs = value; + this.logTree = []; if (!value) return; const empty = {}; const validations = window.validations; - const castJsonValue = this.castJsonValue; - for (const log of value) { + let originLog; + let userLog; + let modelLog; + let nLogs; + + for (let i = 0; i < value.length; i++) { + const log = value[i]; + const prevLog = i > 0 ? value[i - 1] : null; + const locale = validations[log.changedModel]?.locale || empty; + + // Origin + + const originChanged = !prevLog + || log.originFk != prevLog.originFk; + if (originChanged) { + this.logTree.push(originLog = { + originFk: log.originFk, + logs: [] + }); + } + + // User + + const userChanged = originChanged + || log.userFk != prevLog.userFk + || nLogs >= 5; + if (userChanged) { + originLog.logs.push(userLog = { + user: log.user, + userFk: log.userFk, + logs: [] + }); + nLogs = 0; + } + nLogs++; + + // Model + + const modelChanged = userChanged + || log.changedModel != prevLog.changedModel + || log.changedModelId != prevLog.changedModelId; + if (modelChanged) { + userLog.logs.push(modelLog = { + model: log.changedModel, + modelI18n: firstUpper(locale.name) || log.changedModel, + id: log.changedModelId, + showValue: log.changedModelValue, + logs: [] + }); + } + + modelLog.logs.push(log); + + // Changes + const notDelete = log.action != 'delete'; const olds = (notDelete ? log.oldInstance : null) || empty; const vals = (notDelete ? log.newInstance : log.oldInstance) || empty; - const locale = validations[log.changedModel]?.locale || empty; - log.changedModelI18n = firstUpper(locale.name) || log.changedModel; - let props = Object.keys(olds).concat(Object.keys(vals)); - props = [...new Set(props)]; + let propNames = Object.keys(olds).concat(Object.keys(vals)); + propNames = [...new Set(propNames)]; - log.props = []; - for (const prop of props) { - if (prop.endsWith('$')) continue; - log.props.push({ - name: prop, - nameI18n: firstUpper(locale.columns?.[prop]) || prop, - old: getVal(olds, prop), - val: getVal(vals, prop) - }); - } - log.props.sort( - (a, b) => a.nameI18n.localeCompare(b.nameI18n)); - } - - function getVal(vals, prop) { - let val, id; - const showProp = `${prop}$`; - - if (vals[showProp] != null) { - val = vals[showProp]; - id = vals[prop]; - } else - val = vals[prop]; - - return {val: castJsonValue(val), id}; + log.props = this.parseProps(propNames, locale, vals, olds); } } @@ -114,17 +164,76 @@ export default class Controller extends Section { set models(value) { this._models = value; if (!value) return; - for (const model of value) { - const name = model.changedModel; - model.changedModelI18n = - firstUpper(window.validations[name]?.locale?.name) || name; - } + for (const model of value) + model.changedModelI18n = this.translateModel(model.changedModel); } get showModelName() { return !(this.changedModel && this.changedModelId); } + parseProps(propNames, locale, vals, olds) { + const castJsonValue = this.castJsonValue; + const props = []; + + for (const prop of propNames) { + if (prop.endsWith('$')) continue; + props.push({ + name: prop, + nameI18n: firstUpper(locale.columns?.[prop]) || prop, + val: getVal(vals, prop), + old: olds && getVal(olds, prop) + }); + } + props.sort( + (a, b) => a.nameI18n.localeCompare(b.nameI18n)); + + function getVal(vals, prop) { + let val; let id; + const showProp = `${prop}$`; + + if (vals[showProp] != null) { + val = vals[showProp]; + id = vals[prop]; + } else + val = vals[prop]; + + return {val: castJsonValue(val), id}; + } + + return props; + } + + viewPitInstance(event, id, modelLog) { + if (this.instance?.canceler) + this.instance.canceler.resolve(); + + const canceler = this.$q.defer(); + this.instance = { + modelLog, + canceler + }; + const options = {timeout: canceler.promise}; + + this.$http.get(`${this.url}/${id}/pitInstance`, options) + .then(res => { + const instance = res.data; + const propNames = Object.keys(instance); + const locale = window.validations[modelLog.model]?.locale || {}; + this.instance.props = this.parseProps(propNames, locale, instance); + }) + .finally(() => { + this.instance.canceler = null; + this.$.$applyAsync(() => this.$.instancePopover.relocate()); + }); + + this.$.instancePopover.show(event); + } + + translateModel(name) { + return firstUpper(window.validations[name]?.locale?.name) || name; + } + castJsonValue(value) { return typeof value === 'string' && validDate.test(value) ? new Date(value) @@ -160,12 +269,11 @@ export default class Controller extends Section { applyFilter() { const filter = this.$.filter; - function getParam(prop, value) { + const getParam = (prop, value) => { if (value == null || value == '') return null; switch (prop) { case 'search': - const or = []; - if (/^\s*[0-9]+\s*$/.test(value)) + if (/^\s*[0-9]+\s*$/.test(value) || this.byRecord) return {changedModelId: value.trim()}; else return {changedModelValue: {like: `%${value}%`}}; @@ -177,72 +285,86 @@ export default class Controller extends Section { ]}; case 'who': switch (value) { - case 'all': - return null; case 'user': return {userFk: {neq: null}}; case 'system': return {userFk: null}; + case 'all': + default: + return null; } - case 'actions': + case 'actions': { const inq = []; for (const action in value) { if (value[action]) inq.push(action); } return inq.length ? {action: {inq}} : null; + } case 'from': - if (filter.to) { + if (filter.to) return {creationDate: {gte: value}}; - } else { + else { const to = new Date(value); to.setHours(23, 59, 59, 999); return {creationDate: {between: [value, to]}}; } - case 'to': + case 'to': { const to = new Date(value); to.setHours(23, 59, 59, 999); return {creationDate: {lte: to}}; + } case 'userFk': return filter.who != 'system' ? {[prop]: value} : null; default: return {[prop]: value}; } - } + }; + this.hasFilter = false; const and = []; + + if (!filter.search || !filter.changedModel) + this.byRecord = false; + if (!this.byRecord) + and.push({originFk: this.originId}); + for (const prop in filter) { const param = getParam(prop, filter[prop]); - if (param) and.push(param); + if (param) { + and.push(param); + this.hasFilter = true; + } } const lbFilter = and.length ? {where: {and}} : null; return this.$.model.applyFilter(lbFilter); } - filterByEntity(log) { + filterByRecord(modelLog) { + this.byRecord = true; this.$.filter = { who: 'all', - search: log.changedModelId, - changedModel: log.changedModel + search: modelLog.id, + changedModel: modelLog.model }; } searchUser(search) { - if (/^[0-9]+$/.test(search)) { + if (/^[0-9]+$/.test(search)) return {id: search}; - } else { + else { return {or: [ {name: search}, {nickname: {like: `%${search}%`}} - ]} + ]}; } } - showWorkerDescriptor(event, log) { - if (log.user?.worker) - this.$.workerDescriptor.show(event.target, log.userFk); + showWorkerDescriptor(event, userLog) { + if (userLog.user?.worker) + this.$.workerDescriptor.show(event.target, userLog.userFk); } } diff --git a/front/salix/components/log/locale/es.yml b/front/salix/components/log/locale/es.yml index 90a42911b..09154f04e 100644 --- a/front/salix/components/log/locale/es.yml +++ b/front/salix/components/log/locale/es.yml @@ -24,4 +24,5 @@ Changes: Cambios today: hoy yesterday: ayer Show all record changes: Mostrar todos los cambios realizados en el registro +View record at this point in time: Ver el registro en este punto Quit filter: Quitar filtro diff --git a/front/salix/components/log/style.scss b/front/salix/components/log/style.scss index de294972d..d729d09a3 100644 --- a/front/salix/components/log/style.scss +++ b/front/salix/components/log/style.scss @@ -1,13 +1,49 @@ @import "util"; vn-log { - .change { + .origin-log { + &:first-child > .origin-info { + margin-top: 0; + } + & > .origin-info { + display: flex; + align-items: center; + margin-top: 28px; + gap: 6px; + + & > .origin-id { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: $color-font-secondary; + margin: 0; + } + & > .line { + flex-grow: 1; + background-color: $color-font-secondary; + height: 2px; + } + } + } + .user-log { display: flex; - & > .left { + & > .timeline { position: relative; padding-right: 10px; + width: 38px; + min-width: 38px; + flex-grow: auto; + & > .arrow { + height: 8px; + width: 8px; + position: absolute; + transform: rotateY(0deg) rotate(45deg); + top: 15px; + right: -4px; + z-index: 1; + } & > vn-avatar { cursor: pointer; @@ -15,153 +51,187 @@ vn-log { background-color: $color-main !important; } } - & > .arrow { - height: 8px; - width: 8px; - position: absolute; - transform: rotateY(0deg) rotate(45deg); - top: 18px; - right: -4px; - z-index: 1; - } & > .line { position: absolute; background-color: $color-main; width: 2px; - left: 17px; + left: 18px; z-index: -1; top: 44px; - bottom: -8px; + bottom: -2px; } } - &:last-child > .left > .line { + &:last-child > .timeline > .line { display: none; } - .detail { - position: relative; + & > .user-changes { flex-grow: 1; - width: 100%; - border-radius: 2px; overflow: hidden; + } + } + .model-log { + & > .model-info { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + min-height: 22px; - & > .header { - display: flex; - justify-content: space-between; - align-items: center; - overflow: hidden; + & > .model-name { + display: inline-block; + padding: 2px 5px; + color: $color-font-dark; + border-radius: 8px; + vertical-align: middle; + } + & > .model-value { + font-style: italic; + } + & > .model-id { + color: $color-font-secondary; + font-size: .9rem; + } + & > vn-icon[icon="filter_alt"] { + @extend %clickable-light; + vertical-align: middle; + font-size: 18px; + color: $color-font-secondary; + float: right; + display: none; - & > .action-model { - display: inline-flex; - overflow: hidden; - - & > .model-name { - display: inline-block; - padding: 2px 5px; - color: $color-font-dark; - border-radius: 8px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - & > .action-date { - white-space: nowrap; - - & > .action { - display: inline-flex; - align-items: center; - justify-content: center; - color: $color-font-bg; - vertical-align: middle; - border-radius: 50%; - width: 24px; - height: 24px; - font-size: 18px; - - &.notice { - background-color: $color-notice-medium - } - &.success { - background-color: $color-success-medium; - } - &.warning { - background-color: $color-main-medium; - } - &.alert { - background-color: lighten($color-alert, 5%); - } - } + @include mobile { + display: initial; } } - & > .model { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-height: 18px; + } + &:hover > .model-info > vn-icon[icon="filter_alt"] { + display: initial; + } + } + .changes-log { + position: relative; + max-width: 100%; + width: 100%; + border-radius: 2px; + overflow: hidden; - & > vn-icon { + &:last-child { + margin-bottom: 0; + } + & > .change-info { + display: flex; + justify-content: space-between; + align-items: center; + overflow: hidden; + + & > .date { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + & > div { + white-space: nowrap; + + & > vn-icon.pit { @extend %clickable-light; vertical-align: middle; - padding: 2px; - margin: -2px; - font-size: 18px; + font-size: 20px; color: $color-font-secondary; - float: right; display: none; @include mobile { - display: initial; + display: inline-block; } } - & > .model-value { - font-style: italic; - } - & > .model-id { - color: $color-font-secondary; - font-size: .9rem; - } - } - &:hover > .model > vn-icon { - display: initial; - } - } - } - .changes { - overflow: hidden; - background-color: rgba(255, 255, 255, .05); - color: $color-font-light; - position: relative; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-height: 34px; - box-sizing: border-box; + & > .action { + display: inline-flex; + align-items: center; + justify-content: center; + color: $color-font-bg; + vertical-align: middle; + border-radius: 50%; + width: 24px; + height: 24px; + font-size: 18px; - & > vn-icon { - @extend %clickable; - float: right; - position: relative; - transition-property: transform, background-color; - transition-duration: 150ms; - margin: -5px; - margin-left: 4px; - padding: 1px; - border-radius: 50%; + &.notice { + background-color: $color-notice-medium + } + &.success { + background-color: $color-success-medium; + } + &.warning { + background-color: $color-main-medium; + } + &.alert { + background-color: lighten($color-alert, 5%); + } + } + } + &:hover vn-icon.pit { + display: inline-block; + } } - &.expanded { - text-overflow: initial; - white-space: initial; + & > .change-detail { + overflow: hidden; + background-color: rgba(255, 255, 255, .05); + color: $color-font-light; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-height: 34px; + box-sizing: border-box; & > vn-icon { - transform: rotate(180deg); + @extend %clickable; + float: right; + position: relative; + transition-property: transform, background-color; + transition-duration: 150ms; + margin: -5px; + margin-left: 4px; + padding: 1px; + border-radius: 50%; + } + &.expanded { + text-overflow: initial; + white-space: initial; + + & > vn-icon { + transform: rotate(180deg); + } + } + & > .no-changes { + font-style: italic; } } - & > .no-changes { - font-style: italic; + } + .id-value { + font-size: .9rem; + color: $color-font-secondary; + } +} +.vn-log-instance { + display: block; + + & > .loading { + display: flex; + justify-content: center; + } + & > .instance { + min-width: 180px; + max-width: 400px; + + & > .header { + background-color: $color-main; + color: $color-font-dark; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0; + } + & > .change-detail { + color: $color-font-light; } } } -vn-log-value > .id-value { - font-size: .9rem; - color: $color-font-secondary; -} diff --git a/loopback/common/methods/log/pitInstance.js b/loopback/common/methods/log/pitInstance.js new file mode 100644 index 000000000..cb671cb7e --- /dev/null +++ b/loopback/common/methods/log/pitInstance.js @@ -0,0 +1,91 @@ +const NotFoundError = require('vn-loopback/util/not-found-error'); + +module.exports = Self => { + Self.remoteMethod('pitInstance', { + description: 'Gets the status of instance at specific point in time', + accepts: [ + { + arg: 'id', + type: 'integer', + description: 'The log id', + required: true + } + ], + returns: { + type: [Self], + root: true + }, + http: { + path: `/:id/pitInstance`, + verb: 'GET' + } + }); + + Self.pitInstance = async function(id) { + const log = await Self.findById(id, { + fields: [ + 'changedModel', + 'changedModelId', + 'creationDate' + ] + }); + if (!log) + throw new NotFoundError(); + + const where = { + changedModel: log.changedModel, + changedModelId: log.changedModelId + }; + + // Fetch creation and all update logs for record up to requested log + + const createdWhere = { + action: 'insert', + creationDate: {lte: log.creationDate} + }; + const createdLog = await Self.findOne({ + fields: ['id', 'creationDate', 'newInstance'], + where: Object.assign(createdWhere, where), + order: 'creationDate DESC, id DESC' + }); + + const instance = {}; + + let logsWhere = { + action: 'update' + }; + if (createdLog) { + Object.assign(instance, createdLog.newInstance); + Object.assign(logsWhere, { + creationDate: {between: [ + createdLog.creationDate, + log.creationDate + ]}, + id: {between: [ + Math.min(id, createdLog.id), + Math.max(id, createdLog.id) + ]} + }); + } else { + Object.assign(logsWhere, { + creationDate: {lte: log.creationDate}, + id: {lte: id} + }); + } + + const logs = await Self.find({ + fields: ['newInstance'], + where: Object.assign(logsWhere, where), + order: 'creationDate, id' + }); + if (!logs.length && !createdLog) + throw new NotFoundError('No logs found for record'); + + // Merge all logs in order into one instance + + for (const log of logs) + Object.assign(instance, log.newInstance); + + return instance; + }; +}; diff --git a/loopback/common/models/log.js b/loopback/common/models/log.js index 0622431a6..b0d3cfc00 100644 --- a/loopback/common/models/log.js +++ b/loopback/common/models/log.js @@ -5,6 +5,7 @@ module.exports = function(Self) { Self.super_.setup.call(this); require('../methods/log/editors')(this); require('../methods/log/models')(this); + require('../methods/log/pitInstance')(this); } }); }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js index 8cc584c94..c4d50a4ee 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js +++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js @@ -24,20 +24,15 @@ module.exports = Self => { Self.createPdf = async function(ctx, id, options) { const models = Self.app.models; + options = typeof options == 'object' + ? Object.assign({}, options) : {}; let tx; - const myOptions = {}; - - if (typeof options == 'object') - Object.assign(myOptions, options); - - if (!myOptions.transaction) { - tx = await Self.beginTransaction({}); - myOptions.transaction = tx; - } + if (!options.transaction) + tx = options.transaction = await Self.beginTransaction({}); try { - const invoiceOut = await Self.findById(id, {fields: ['hasPdf']}, myOptions); + const invoiceOut = await Self.findById(id, {fields: ['hasPdf']}, options); if (invoiceOut.hasPdf) { const canCreatePdf = await models.ACL.checkAccessAcl(ctx, 'InvoiceOut', 'canCreatePdf', 'WRITE'); @@ -45,7 +40,7 @@ module.exports = Self => { throw new UserError(`You don't have enough privileges`); } - await Self.makePdf(id, myOptions); + await Self.makePdf(id, options); if (tx) await tx.commit(); } catch (err) { diff --git a/modules/invoiceOut/back/methods/invoiceOut/download.js b/modules/invoiceOut/back/methods/invoiceOut/download.js index 49bba046b..4c76f7c07 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/download.js +++ b/modules/invoiceOut/back/methods/invoiceOut/download.js @@ -1,6 +1,5 @@ const fs = require('fs-extra'); const path = require('path'); -const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { Self.remoteMethodCtx('download', { @@ -37,47 +36,43 @@ module.exports = Self => { Self.download = async function(ctx, id, options) { const models = Self.app.models; - const myOptions = {}; + options = typeof options == 'object' + ? Object.assign({}, options) : {}; - if (typeof options == 'object') - Object.assign(myOptions, options); + const pdfFile = await Self.filePath(id, options); + + const container = await models.InvoiceContainer.container(pdfFile.year); + const rootPath = container.client.root; + + const file = { + path: path.join(rootPath, pdfFile.path, pdfFile.name), + contentType: 'application/pdf', + name: pdfFile.name + }; try { - const invoiceOut = await models.InvoiceOut.findById(id, { - fields: ['ref', 'issued'] - }, myOptions); + await fs.access(file.path); + } catch (error) { + await Self.createPdf(ctx, id, options); + } - const issued = invoiceOut.issued; - const year = issued.getFullYear().toString(); - const month = (issued.getMonth() + 1).toString(); - const day = issued.getDate().toString(); - - const container = await models.InvoiceContainer.container(year); - const rootPath = container.client.root; - const src = path.join(rootPath, year, month, day); - const fileName = `${year}${invoiceOut.ref}.pdf`; - const fileSrc = path.join(src, fileName); - - const file = { - path: fileSrc, - contentType: 'application/pdf', - name: fileName - }; + let stream = await fs.createReadStream(file.path); + // XXX: To prevent unhandled ENOENT error + // https://stackoverflow.com/questions/17136536/is-enoent-from-fs-createreadstream-uncatchable + stream.on('error', err => { + const e = new Error(err.message); + err.stack = e.stack; + console.error(err); + }); + if (process.env.NODE_ENV == 'test') { try { await fs.access(file.path); } catch (error) { - await Self.createPdf(ctx, id, myOptions); + stream = null; } - - const stream = fs.createReadStream(file.path); - - return [stream, file.contentType, `filename="${file.name}"`]; - } catch (error) { - if (error.code === 'ENOENT') - throw new UserError('The PDF document does not exist'); - - throw error; } + + return [stream, file.contentType, `filename="${pdfFile.name}"`]; }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js index ab076b46f..ddd008942 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js @@ -45,7 +45,6 @@ module.exports = Self => { Self.invoiceClient = async(ctx, options) => { const args = ctx.args; const models = Self.app.models; - options = typeof options == 'object' ? Object.assign({}, options) : {}; options.userId = ctx.req.accessToken.userId; diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js index 113526484..400328fef 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js @@ -11,20 +11,17 @@ module.exports = Self => { type: 'string', required: true, http: {source: 'path'} - }, - { + }, { arg: 'recipient', type: 'string', description: 'The recipient email', required: true, - }, - { + }, { arg: 'replyTo', type: 'string', description: 'The sender email to reply to', required: false - }, - { + }, { arg: 'recipientId', type: 'number', description: 'The recipient id to send to the recipient preferred language', @@ -43,16 +40,13 @@ module.exports = Self => { Self.invoiceEmail = async(ctx, reference) => { const args = Object.assign({}, ctx.args); - const {InvoiceOut} = Self.app.models; const params = { recipient: args.recipient, lang: ctx.req.getLocale() }; - const invoiceOut = await InvoiceOut.findOne({ - where: { - ref: reference - } + const invoiceOut = await Self.findOne({ + where: {ref: reference} }); delete args.ctx; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js deleted file mode 100644 index 4029bfbcf..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -const models = require('vn-loopback/server/server').models; -const fs = require('fs-extra'); - -describe('InvoiceOut download()', () => { - const userId = 9; - const invoiceId = 1; - const ctx = { - req: { - - accessToken: {userId: userId}, - headers: {origin: 'http://localhost:5000'}, - } - }; - - it('should return the downloaded file name', async() => { - spyOn(models.InvoiceContainer, 'container').and.returnValue({ - client: {root: '/path'} - }); - spyOn(fs, 'createReadStream').and.returnValue(new Promise(resolve => resolve('streamObject'))); - spyOn(fs, 'access').and.returnValue(true); - spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true))); - - const result = await models.InvoiceOut.download(ctx, invoiceId); - - expect(result[1]).toEqual('application/pdf'); - expect(result[2]).toMatch(/filename="\d{4}T1111111.pdf"/); - }); -}); diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index 9baa1e7e6..d3aaf3b3d 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -1,4 +1,5 @@ const print = require('vn-print'); +const path = require('path'); module.exports = Self => { require('../methods/invoiceOut/filter')(Self); @@ -23,20 +24,31 @@ module.exports = Self => { require('../methods/invoiceOut/negativeBases')(Self); require('../methods/invoiceOut/negativeBasesCsv')(Self); - Self.makePdf = async function(id, options) { - const fields = ['id', 'hasPdf', 'ref', 'issued']; + Self.filePath = async function(id, options) { + const fields = ['ref', 'issued']; const invoiceOut = await Self.findById(id, {fields}, options); - const invoiceReport = new print.Report('invoice', { - reference: invoiceOut.ref - }); - const buffer = await invoiceReport.toPdfStream(); const issued = invoiceOut.issued; const year = issued.getFullYear().toString(); const month = (issued.getMonth() + 1).toString(); const day = issued.getDate().toString(); - const fileName = `${year}${invoiceOut.ref}.pdf`; + return { + path: path.join(year, month, day), + name: `${year}${invoiceOut.ref}.pdf`, + year + }; + }; + + Self.makePdf = async function(id, options) { + const fields = ['id', 'hasPdf', 'ref']; + const invoiceOut = await Self.findById(id, {fields}, options); + const invoiceReport = new print.Report('invoice', { + reference: invoiceOut.ref + }); + const buffer = await invoiceReport.toPdfStream(); + + const pdfFile = await Self.filePath(id, options); // Store invoice @@ -47,8 +59,8 @@ module.exports = Self => { if (process.env.NODE_ENV !== 'test') { await print.storage.write(buffer, { type: 'invoice', - path: `${year}/${month}/${day}`, - fileName: fileName + path: pdfFile.path, + fileName: pdfFile.name }); } }; diff --git a/modules/ticket/back/methods/ticket/setDeleted.js b/modules/ticket/back/methods/ticket/setDeleted.js index 7cc300547..4d7fe73d4 100644 --- a/modules/ticket/back/methods/ticket/setDeleted.js +++ b/modules/ticket/back/methods/ticket/setDeleted.js @@ -81,17 +81,15 @@ module.exports = Self => { throw new UserError('You must delete all the buy requests first'); // removes item shelvings - if (hasItemShelvingSales && isSalesAssistant) { - const promises = []; - for (let sale of sales) { - if (sale.itemShelvingSale()) { - const itemShelvingSale = sale.itemShelvingSale(); - const destroyedShelving = models.ItemShelvingSale.destroyById(itemShelvingSale.id, myOptions); - promises.push(destroyedShelving); - } + const promises = []; + for (let sale of sales) { + if (sale.itemShelvingSale()) { + const itemShelvingSale = sale.itemShelvingSale(); + const destroyedShelving = models.ItemShelvingSale.destroyById(itemShelvingSale.id, myOptions); + promises.push(destroyedShelving); } - await Promise.all(promises); } + await Promise.all(promises); // Remove ticket greuges const ticketGreuges = await models.Greuge.find({where: {ticketFk: id}}, myOptions); diff --git a/modules/worker/back/models/operator.js b/modules/worker/back/models/operator.js index e9b590fa6..db1ac7e49 100644 --- a/modules/worker/back/models/operator.js +++ b/modules/worker/back/models/operator.js @@ -1,10 +1,10 @@ module.exports = function(Self) { Self.observe('after save', async function(ctx) { - const instance = ctx.instance; + const instance = ctx.data || ctx.instance; const models = Self.app.models; const options = ctx.options; - if (!instance.sectorFk || !instance.labelerFk) return; + if (!instance?.sectorFk || !instance?.labelerFk) return; const sector = await models.Sector.findById(instance.sectorFk, { fields: ['mainPrinterFk'] diff --git a/modules/worker/back/models/operator.json b/modules/worker/back/models/operator.json index 9433a0fd5..2417078e1 100644 --- a/modules/worker/back/models/operator.json +++ b/modules/worker/back/models/operator.json @@ -16,18 +16,12 @@ "type": "number" }, "trainFk": { - "type": "number", - "required": true + "type": "number" }, "itemPackingTypeFk": { - "type": "string", - "required": true + "type": "string" }, "warehouseFk": { - "type": "number", - "required": true - }, - "sectorFk": { "type": "number" }, "labelerFk": { diff --git a/print/core/email.js b/print/core/email.js index 6e96f5c2e..43e92e619 100644 --- a/print/core/email.js +++ b/print/core/email.js @@ -63,12 +63,12 @@ class Email extends Component { await getAttachments(componentPath, component.attachments); if (component.components) - await getSubcomponentAttachments(component) + await getSubcomponentAttachments(component); } } } - await getSubcomponentAttachments(instance) + await getSubcomponentAttachments(instance); if (this.attachments) await getAttachments(this.path, this.attachments); diff --git a/print/core/smtp.js b/print/core/smtp.js index 04bede5b5..7ff9ca41d 100644 --- a/print/core/smtp.js +++ b/print/core/smtp.js @@ -20,14 +20,18 @@ module.exports = { options.to = config.app.senderEmail; } + let res; let error; - return this.transporter.sendMail(options).catch(err => { + try { + res = await this.transporter.sendMail(options); + } catch (err) { error = err; - throw err; - }).finally(async() => { + } finally { await this.mailLog(options, error); - }); + } + + return res; }, async mailLog(options, error) {