diff --git a/back/methods/account/change-password.js b/back/methods/account/change-password.js index 25b63b9a8e..c0956b1937 100644 --- a/back/methods/account/change-password.js +++ b/back/methods/account/change-password.js @@ -5,17 +5,17 @@ module.exports = Self => { accepts: [ { arg: 'id', - type: 'Number', + type: 'number', description: 'The user id', http: {source: 'path'} }, { arg: 'oldPassword', - type: 'String', + type: 'string', description: 'The old password', required: true }, { arg: 'newPassword', - type: 'String', + type: 'string', description: 'The new password', required: true } diff --git a/back/methods/edi/sql/bucket.sql b/back/methods/edi/sql/bucket.sql index 92121386cb..33dca67968 100644 --- a/back/methods/edi/sql/bucket.sql +++ b/back/methods/edi/sql/bucket.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE bucket + INTO TABLE `edi`.`bucket` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9, @col10, @col11, @col12) SET diff --git a/back/methods/edi/sql/bucket_type.sql b/back/methods/edi/sql/bucket_type.sql index 68498f1837..f748fac84f 100644 --- a/back/methods/edi/sql/bucket_type.sql +++ b/back/methods/edi/sql/bucket_type.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE bucket_type + INTO TABLE `edi`.`bucket_type` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6) SET diff --git a/back/methods/edi/sql/feature.sql b/back/methods/edi/sql/feature.sql index 081cfde7b9..7486a5bd34 100644 --- a/back/methods/edi/sql/feature.sql +++ b/back/methods/edi/sql/feature.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE `feature` + INTO TABLE `edi`.`feature` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7) SET diff --git a/back/methods/edi/sql/genus.sql b/back/methods/edi/sql/genus.sql index dd882629cd..52b85f0204 100644 --- a/back/methods/edi/sql/genus.sql +++ b/back/methods/edi/sql/genus.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE genus + INTO TABLE `edi`.`genus` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6) SET diff --git a/back/methods/edi/sql/item.sql b/back/methods/edi/sql/item.sql index 543c83381a..8d794c7cfa 100644 --- a/back/methods/edi/sql/item.sql +++ b/back/methods/edi/sql/item.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE item + INTO TABLE `edi`.`item` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9, @col10, @col11, @col12) SET diff --git a/back/methods/edi/sql/item_feature.sql b/back/methods/edi/sql/item_feature.sql index 23b0f3afa8..f16da6ed01 100644 --- a/back/methods/edi/sql/item_feature.sql +++ b/back/methods/edi/sql/item_feature.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE `item_feature` + INTO TABLE `edi`.`item_feature` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8) SET diff --git a/back/methods/edi/sql/item_group.sql b/back/methods/edi/sql/item_group.sql index 31da6c57fd..1a57d77273 100644 --- a/back/methods/edi/sql/item_group.sql +++ b/back/methods/edi/sql/item_group.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE item_group + INTO TABLE `edi`.`item_group` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6) SET diff --git a/back/methods/edi/sql/plant.sql b/back/methods/edi/sql/plant.sql index 50720fba5c..8d6f90b784 100644 --- a/back/methods/edi/sql/plant.sql +++ b/back/methods/edi/sql/plant.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE plant + INTO TABLE `edi`.`plant` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9) SET diff --git a/back/methods/edi/sql/specie.sql b/back/methods/edi/sql/specie.sql index e27478ae8e..956ec2b699 100644 --- a/back/methods/edi/sql/specie.sql +++ b/back/methods/edi/sql/specie.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE specie + INTO TABLE `edi`.`specie` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7) SET diff --git a/back/methods/edi/sql/supplier.sql b/back/methods/edi/sql/supplier.sql index a1d3376d61..aa2ddc4d8c 100644 --- a/back/methods/edi/sql/supplier.sql +++ b/back/methods/edi/sql/supplier.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE edi.supplier + INTO TABLE `edi`.`supplier` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9, @col10, @col11, @col12, @col13, @col14, @col15, @col16, @col17, @col18, @col19, @col20) SET diff --git a/back/methods/edi/sql/type.sql b/back/methods/edi/sql/type.sql index 88c4ac026c..1ff60fb0b3 100644 --- a/back/methods/edi/sql/type.sql +++ b/back/methods/edi/sql/type.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE `type` + INTO TABLE `edi`.`type` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7) SET diff --git a/back/methods/edi/sql/value.sql b/back/methods/edi/sql/value.sql index c8c4deef51..2a469f201e 100644 --- a/back/methods/edi/sql/value.sql +++ b/back/methods/edi/sql/value.sql @@ -1,5 +1,5 @@ LOAD DATA LOCAL INFILE ? - INTO TABLE `value` + INTO TABLE `edi`.`value` FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7) SET diff --git a/back/methods/edi/updateData.js b/back/methods/edi/updateData.js index df77906daa..33ad76e6db 100644 --- a/back/methods/edi/updateData.js +++ b/back/methods/edi/updateData.js @@ -22,6 +22,9 @@ module.exports = Self => { const container = await models.TempContainer.container('edi'); const tempPath = path.join(container.client.root, container.name); + // Temporary file clean + await fs.rmdir(`${tempPath}/*`, {recursive: true}); + const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig'); console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`); @@ -48,6 +51,12 @@ module.exports = Self => { tempDir = `${tempPath}/${fileName}`; tempFile = `${tempPath}/${fileName}.zip`; + // if (fs.existsSync(tempFile)) + // await fs.unlink(tempFile); + + // if (fs.existsSync(tempDir)) + // await fs.rmdir(tempDir, {recursive: true}); + await extractFile({ ftpClient: ftpClient, file: file, @@ -61,7 +70,6 @@ module.exports = Self => { if (fs.existsSync(tempFile)) await fs.unlink(tempFile); - await fs.rmdir(tempDir, {recursive: true}); console.error(error); } } @@ -86,9 +94,6 @@ module.exports = Self => { zip.extractAllTo(paths.tempDir, false); - if (fs.existsSync(paths.tempFile)) - await fs.unlink(paths.tempFile); - await dumpData({file, entries, paths}); await fs.rmdir(paths.tempDir, {recursive: true}); @@ -99,57 +104,59 @@ module.exports = Self => { const toTable = file.toTable; const baseName = file.fileName; - for (const zipEntry of entries) { - const entryName = zipEntry.entryName; - console.log(`Reading file ${entryName}...`); + const tx = await Self.beginTransaction({}); - const startIndex = (entryName.length - 10); - const endIndex = (entryName.length - 4); - const dateString = entryName.substring(startIndex, endIndex); - const lastUpdated = new Date(); + try { + const options = {transaction: tx}; - // Format string date to a date object - let updated = null; - if (file.updated) { - updated = new Date(file.updated); - updated.setHours(0, 0, 0, 0); - } + const tableName = `edi.${toTable}`; + await Self.rawSql(`DELETE FROM ??`, [tableName], options); - lastUpdated.setFullYear(`20${dateString.substring(4, 6)}`); - lastUpdated.setMonth(parseInt(dateString.substring(2, 4)) - 1); - lastUpdated.setDate(dateString.substring(0, 2)); - lastUpdated.setHours(0, 0, 0, 0); + for (const zipEntry of entries) { + const entryName = zipEntry.entryName; + console.log(`Reading file ${entryName}...`); - if (updated && lastUpdated <= updated) { - console.debug(`Table ${toTable} already updated, skipping...`); - continue; - } + const startIndex = (entryName.length - 10); + const endIndex = (entryName.length - 4); + const dateString = entryName.substring(startIndex, endIndex); + const lastUpdated = new Date(); - console.log('Dumping data...'); - const templatePath = path.join(__dirname, `./sql/${toTable}.sql`); - const sqlTemplate = fs.readFileSync(templatePath, 'utf8'); + // Format string date to a date object + let updated = null; + if (file.updated) { + updated = new Date(file.updated); + updated.setHours(0, 0, 0, 0); + } - const rawPath = path.join(paths.tempDir, entryName); + lastUpdated.setFullYear(`20${dateString.substring(4, 6)}`); + lastUpdated.setMonth(parseInt(dateString.substring(2, 4)) - 1); + lastUpdated.setDate(dateString.substring(0, 2)); + lastUpdated.setHours(0, 0, 0, 0); - try { - const tx = await Self.beginTransaction({}); - const options = {transaction: tx}; + if (updated && lastUpdated <= updated) { + console.debug(`Table ${toTable} already updated, skipping...`); + continue; + } + + console.log('Dumping data...'); + const templatePath = path.join(__dirname, `./sql/${toTable}.sql`); + const sqlTemplate = fs.readFileSync(templatePath, 'utf8'); + + const rawPath = path.join(paths.tempDir, entryName); - await Self.rawSql(`DELETE FROM edi.${toTable}`, null, options); await Self.rawSql(sqlTemplate, [rawPath], options); await Self.rawSql(` - UPDATE edi.fileConfig - SET updated = ? - WHERE fileName = ? - `, [lastUpdated, baseName], options); - - tx.commit(); - } catch (error) { - tx.rollback(); - throw error; + UPDATE edi.fileConfig + SET updated = ? + WHERE fileName = ? + `, [lastUpdated, baseName], options); } - console.log(`Updated table ${toTable}\n`); + tx.commit(); + } catch (error) { + tx.rollback(); + throw error; } + console.log(`Updated table ${toTable}\n`); } }; diff --git a/db/changes/10470-family/00-aclMdb.sql b/db/changes/10470-family/00-aclMdb.sql new file mode 100644 index 0000000000..c57f60eb37 --- /dev/null +++ b/db/changes/10470-family/00-aclMdb.sql @@ -0,0 +1,14 @@ +CREATE TABLE `vn`.`mdbBranch` ( + `name` VARCHAR(255), + PRIMARY KEY(`name`) +); + +CREATE TABLE `vn`.`mdbVersion` ( + `app` VARCHAR(255) NOT NULL, + `branchFk` VARCHAR(255) NOT NULL, + `version` INT, + CONSTRAINT `mdbVersion_branchFk` FOREIGN KEY (`branchFk`) REFERENCES `vn`.`mdbBranch` (`name`) ON DELETE CASCADE ON UPDATE CASCADE +); + +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES('MdbVersion', '*', '*', 'ALLOW', 'ROLE', 'developer'); diff --git a/db/changes/10470-family/00-defaultViewConfig.sql b/db/changes/10470-family/00-defaultViewConfig.sql new file mode 100644 index 0000000000..d423599b17 --- /dev/null +++ b/db/changes/10470-family/00-defaultViewConfig.sql @@ -0,0 +1,3 @@ +INSERT INTO `salix`.`defaultViewConfig` (tableCode, columns) +VALUES ('clientsDetail', '{"id":true,"phone":true,"city":true,"socialName":true,"salesPersonFk":true,"email":true,"name":false,"fi":false,"credit":false,"creditInsurance":false,"mobile":false,"street":false,"countryFk":false,"provinceFk":false,"postcode":false,"created":false,"businessTypeFk":false,"payMethodFk":false,"sageTaxTypeFk":false,"sageTransactionTypeFk":false,"isActive":false,"isVies":false,"isTaxDataChecked":false,"isEqualizated":false,"isFreezed":false,"hasToInvoice":false,"hasToInvoiceByAddress":false,"isToBeMailed":false,"hasLcr":false,"hasCoreVnl":false,"hasSepaVnl":false}'); + diff --git a/db/changes/10451-april/00-ticket_doRefund.sql b/db/changes/10470-family/00-ticket_doRefund.sql similarity index 98% rename from db/changes/10451-april/00-ticket_doRefund.sql rename to db/changes/10470-family/00-ticket_doRefund.sql index 5725b4fe52..f4ecf29d71 100644 --- a/db/changes/10451-april/00-ticket_doRefund.sql +++ b/db/changes/10470-family/00-ticket_doRefund.sql @@ -1,4 +1,4 @@ -DROP PROCEDURE IF EXISTS vn.ticket_doRefund; +DROP PROCEDURE IF EXISTS `vn`.`ticket_doRefund`; DELIMITER $$ $$ diff --git a/db/changes/10470-family/delete.keep b/db/changes/10470-family/delete.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 225484ff06..a245f6f902 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2583,3 +2583,12 @@ INSERT INTO `vn`.`machineWorker` (`workerFk`, `machineFk`, `inTimed`, `outTimed` (1106, 2, CURDATE(), NULL), (1106, 2, DATE_ADD(CURDATE(), INTERVAL + 1 DAY), DATE_ADD(CURDATE(), INTERVAL +1 DAY)); +INSERT INTO `vn`.`mdbBranch` (`name`) + VALUES + ('test'), + ('master'); + +INSERT INTO `vn`.`mdbVersion` (`app`, `branchFk`, `version`) + VALUES + ('tpv', 'test', '1'), + ('lab', 'master', '1'); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1d80a4b62d..4fc5dc8117 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,7 @@ services: - /mnt/appdata/pdfs:/var/lib/salix/pdfs - /mnt/appdata/dms:/var/lib/salix/dms - /mnt/appdata/image:/var/lib/salix/image + - /mnt/appdata/vn-access:/var/lib/salix/vn-access deploy: replicas: ${BACK_REPLICAS:?} placement: diff --git a/e2e/paths/02-client/12_lock_of_verified_data.spec.js b/e2e/paths/02-client/12_lock_of_verified_data.spec.js index af42e2a4bd..139af0cea5 100644 --- a/e2e/paths/02-client/12_lock_of_verified_data.spec.js +++ b/e2e/paths/02-client/12_lock_of_verified_data.spec.js @@ -123,19 +123,19 @@ describe('Client lock verified data path', () => { await page.accessToSection('client.card.fiscalData'); }, 20000); - it('should confirm verified data button is disabled for salesAssistant', async() => { + it('should confirm verified data button is enabled for salesAssistant', async() => { const isDisabled = await page.isDisabled(selectors.clientFiscalData.verifiedDataCheckbox); - expect(isDisabled).toBeTrue(); + expect(isDisabled).toBeFalsy(); }); - it('should return error when edit the social name', async() => { + it('should now edit the social name', async() => { await page.clearInput(selectors.clientFiscalData.socialName); await page.write(selectors.clientFiscalData.socialName, 'new social name edition'); await page.waitToClick(selectors.clientFiscalData.saveButton); const message = await page.waitForSnackbar(); - expect(message.text).toContain(`Not enough privileges to edit a client with verified data`); + expect(message.text).toContain(`Data saved!`); }); it('should now confirm the social name have been edited once and for all', async() => { diff --git a/front/core/components/smart-table/index.js b/front/core/components/smart-table/index.js index 81d8d103e5..401541c2ca 100644 --- a/front/core/components/smart-table/index.js +++ b/front/core/components/smart-table/index.js @@ -318,6 +318,8 @@ export default class SmartTable extends Component { for (let column of columns) { const field = column.getAttribute('field'); const cell = document.createElement('td'); + cell.setAttribute('centered', ''); + if (field) { let input; let options; @@ -331,6 +333,15 @@ export default class SmartTable extends Component { continue; } + input = this.$compile(` + <vn-textfield + class="dense" + name="${field}" + ng-model="searchProps['${field}']" + ng-keydown="$ctrl.searchWithEvent($event, '${field}')" + clear-disabled="true" + />`)(this.$inputsScope); + if (options && options.autocomplete) { let props = ``; @@ -346,16 +357,29 @@ export default class SmartTable extends Component { on-change="$ctrl.searchByColumn('${field}')" clear-disabled="true" />`)(this.$inputsScope); - } else { + } + + if (options && options.checkbox) { input = this.$compile(` - <vn-textfield + <vn-check class="dense" name="${field}" ng-model="searchProps['${field}']" - ng-keydown="$ctrl.searchWithEvent($event, '${field}')" - clear-disabled="true" + on-change="$ctrl.searchByColumn('${field}')" + triple-state="true" />`)(this.$inputsScope); } + + if (options && options.datepicker) { + input = this.$compile(` + <vn-date-picker + class="dense" + name="${field}" + ng-model="searchProps['${field}']" + on-change="$ctrl.searchByColumn('${field}')" + />`)(this.$inputsScope); + } + cell.appendChild(input[0]); } searchRow.appendChild(cell); @@ -372,13 +396,12 @@ export default class SmartTable extends Component { searchByColumn(field) { const searchCriteria = this.$inputsScope.searchProps[field]; - const emptySearch = searchCriteria == '' || null; + const emptySearch = searchCriteria === '' || searchCriteria == null; const filters = this.filterSanitizer(field); if (filters && filters.userFilter) this.model.userFilter = filters.userFilter; - if (!emptySearch) this.addFilter(field, this.$inputsScope.searchProps[field]); else this.model.refresh(); diff --git a/loopback/locale/es.json b/loopback/locale/es.json index a44ba2da81..9e2b8989b9 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -224,5 +224,7 @@ "The agency is already assigned to another autonomous": "La agencia ya está asignada a otro autónomo", "date in the future": "Fecha en el futuro", "reference duplicated": "Referencia duplicada", - "This ticket is already a refund": "Este ticket ya es un abono" + "This ticket is already a refund": "Este ticket ya es un abono", + "isWithoutNegatives": "isWithoutNegatives", + "routeFk": "routeFk" } \ No newline at end of file diff --git a/loopback/server/datasources.json b/loopback/server/datasources.json index f51beeb194..5dade9c2e9 100644 --- a/loopback/server/datasources.json +++ b/loopback/server/datasources.json @@ -98,5 +98,15 @@ "image/jpg", "video/mp4" ] + }, + "accessStorage": { + "name": "accessStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/access", + "maxFileSize": "524288000", + "allowedContentTypes": [ + "application/x-7z-compressed" + ] } } \ No newline at end of file diff --git a/modules/claim/back/methods/claim/regularizeClaim.js b/modules/claim/back/methods/claim/regularizeClaim.js index 29c7320f54..ab8ea58a44 100644 --- a/modules/claim/back/methods/claim/regularizeClaim.js +++ b/modules/claim/back/methods/claim/regularizeClaim.js @@ -86,7 +86,6 @@ module.exports = Self => { }; ticketFk = await createTicket(ctx, myOptions); } - await models.Sale.create({ ticketFk: ticketFk, itemFk: sale.itemFk, diff --git a/modules/claim/front/summary/index.html b/modules/claim/front/summary/index.html index 282c55b006..5d90da516a 100644 --- a/modules/claim/front/summary/index.html +++ b/modules/claim/front/summary/index.html @@ -1,5 +1,6 @@ <vn-crud-model vn-id="model" url="ClaimDms" + filter="::$ctrl.filter" data="photos"> </vn-crud-model> <vn-card class="summary"> @@ -106,8 +107,13 @@ <section class="photo" ng-repeat="photo in photos"> <section class="image" on-error-src ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}" - zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}"> + zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}" + ng-if="photo.dms.contentType != 'video/mp4'"> </section> + <video id="videobcg" muted="muted" controls ng-if="photo.dms.contentType == 'video/mp4'" + class="video"> + <source src="{{$ctrl.getImagePath(photo.dmsFk)}}" type="video/mp4"> + </video> </section> </vn-horizontal> </vn-auto> diff --git a/modules/claim/front/summary/index.js b/modules/claim/front/summary/index.js index 721f518469..7cd4805e9f 100644 --- a/modules/claim/front/summary/index.js +++ b/modules/claim/front/summary/index.js @@ -6,6 +6,13 @@ class Controller extends Summary { constructor($element, $, vnFile) { super($element, $); this.vnFile = vnFile; + this.filter = { + include: [ + { + relation: 'dms' + } + ] + }; } $onChanges() { diff --git a/modules/claim/front/summary/style.scss b/modules/claim/front/summary/style.scss index e812136584..5b4e32f7ab 100644 --- a/modules/claim/front/summary/style.scss +++ b/modules/claim/front/summary/style.scss @@ -10,4 +10,19 @@ vn-claim-summary { vn-textarea *{ height: 80px; } + + .video { + width: 100%; + height: 100%; + object-fit: cover; + cursor: pointer; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), + 0 3px 1px -2px rgba(0,0,0,.2), + 0 1px 5px 0 rgba(0,0,0,.12); + border: 2px solid transparent; + + } + .video:hover { + border: 2px solid $color-primary + } } \ No newline at end of file diff --git a/modules/client/back/methods/client/extendedListFilter.js b/modules/client/back/methods/client/extendedListFilter.js new file mode 100644 index 0000000000..8e02cd413f --- /dev/null +++ b/modules/client/back/methods/client/extendedListFilter.js @@ -0,0 +1,159 @@ + +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; +const buildFilter = require('vn-loopback/util/filter').buildFilter; +const mergeFilters = require('vn-loopback/util/filter').mergeFilters; + +module.exports = Self => { + Self.remoteMethodCtx('extendedListFilter', { + description: 'Find all clients matched by the filter', + accessType: 'READ', + accepts: [ + { + arg: 'filter', + type: 'object', + }, + { + arg: 'search', + type: 'string', + description: `If it's and integer searchs by id, otherwise it searchs by name`, + }, + { + arg: 'name', + type: 'string', + description: 'The client name', + }, + { + arg: 'salesPersonFk', + type: 'number', + }, + { + arg: 'fi', + type: 'string', + description: 'The client fiscal id', + }, + { + arg: 'socialName', + type: 'string', + }, + { + arg: 'city', + type: 'string', + }, + { + arg: 'postcode', + type: 'string', + }, + { + arg: 'provinceFk', + type: 'number', + }, + { + arg: 'email', + type: 'string', + }, + { + arg: 'phone', + type: 'string', + }, + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/extendedListFilter`, + verb: 'GET' + } + }); + + Self.extendedListFilter = async(ctx, filter, options) => { + const conn = Self.dataSource.connector; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const where = buildFilter(ctx.args, (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {'c.id': {inq: value}} + : {'c.name': {like: `%${value}%`}}; + case 'name': + case 'salesPersonFk': + case 'fi': + case 'socialName': + case 'city': + case 'postcode': + case 'provinceFk': + case 'email': + case 'phone': + param = `c.${param}`; + return {[param]: value}; + } + }); + + filter = mergeFilters(filter, {where}); + + const stmts = []; + const stmt = new ParameterizedSQL( + `SELECT + c.id, + c.name, + c.socialName, + c.fi, + c.credit, + c.creditInsurance, + c.phone, + c.mobile, + c.street, + c.city, + c.postcode, + c.email, + c.created, + c.isActive, + c.isVies, + c.isTaxDataChecked, + c.isEqualizated, + c.isFreezed, + c.hasToInvoice, + c.hasToInvoiceByAddress, + c.isToBeMailed, + c.hasSepaVnl, + c.hasLcr, + c.hasCoreVnl, + ct.id AS countryFk, + ct.country, + p.id AS provinceFk, + p.name AS province, + u.id AS salesPersonFk, + u.name AS salesPerson, + bt.code AS businessTypeFk, + bt.description AS businessType, + pm.id AS payMethodFk, + pm.name AS payMethod, + sti.CodigoIva AS sageTaxTypeFk, + sti.Iva AS sageTaxType, + stt.CodigoTransaccion AS sageTransactionTypeFk, + stt.Transaccion AS sageTransactionType + FROM client c + LEFT JOIN account.user u ON u.id = c.salesPersonFk + LEFT JOIN country ct ON ct.id = c.countryFk + LEFT JOIN province p ON p.id = c.provinceFk + LEFT JOIN businessType bt ON bt.code = c.businessTypeFk + LEFT JOIN payMethod pm ON pm.id = c.payMethodFk + LEFT JOIN sage.TiposIva sti ON sti.CodigoIva = c.taxTypeSageFk + LEFT JOIN sage.TiposTransacciones stt ON stt.CodigoTransaccion = c.transactionTypeSageFk + ` + ); + + stmt.merge(conn.makeWhere(filter.where)); + stmt.merge(conn.makePagination(filter)); + + const clientsIndex = stmts.push(stmt) - 1; + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await conn.executeStmt(sql, myOptions); + + return clientsIndex === 0 ? result : result[clientsIndex]; + }; +}; diff --git a/modules/client/back/methods/client/specs/extendedListFilter.spec.js b/modules/client/back/methods/client/specs/extendedListFilter.spec.js new file mode 100644 index 0000000000..907c03ef9a --- /dev/null +++ b/modules/client/back/methods/client/specs/extendedListFilter.spec.js @@ -0,0 +1,180 @@ +const { models } = require('vn-loopback/server/server'); + +describe('client extendedListFilter()', () => { + it('should return the clients matching the filter with a limit of 20 rows', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {}}; + const filter = {limit: '20'}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + expect(result.length).toEqual(20); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the search argument with his name', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {search: 'Bruce Wayne'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the search argument with his id', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {search: '1101'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the name argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {name: 'Bruce Wayne'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "salesPersonFk" argument', async() => { + const tx = await models.Client.beginTransaction({}); + const salesPersonId = 18; + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {salesPersonFk: salesPersonId}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(5); + expect(randomResultClient.salesPersonFk).toEqual(salesPersonId); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "fi" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {fi: '251628698'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstClient = result[0]; + + expect(result.length).toEqual(1); + expect(firstClient.name).toEqual('Max Eisenhardt'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "city" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {city: 'Silla'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(20); + expect(randomResultClient.city.toLowerCase()).toEqual('silla'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "postcode" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {postcode: '46460'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(20); + expect(randomResultClient.postcode).toEqual('46460'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/client/back/methods/client/specs/updatePortfolio.spec.js b/modules/client/back/methods/client/specs/updatePortfolio.spec.js index f56555c08c..bf681eb2ed 100644 --- a/modules/client/back/methods/client/specs/updatePortfolio.spec.js +++ b/modules/client/back/methods/client/specs/updatePortfolio.spec.js @@ -1,21 +1,39 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('Client updatePortfolio', () => { - const clientId = 1108; + const activeCtx = { + accessToken: {userId: 9}, + http: { + req: { + headers: {origin: 'http://localhost'}, + [`__`]: value => { + return value; + } + } + } + }; + + beforeAll(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + it('should update the portfolioWeight when the salesPerson of a client changes', async() => { + const clientId = 1108; const salesPersonId = 18; const tx = await models.Client.beginTransaction({}); try { const options = {transaction: tx}; - const expectedResult = 841.63; - const clientQuery = `UPDATE vn.client SET salesPersonFk = ${salesPersonId} WHERE id = ${clientId}; `; - await models.Client.rawSql(clientQuery); + const client = await models.Client.findById(clientId, null, options); + await client.updateAttribute('salesPersonFk', salesPersonId, options); - await models.Client.updatePortfolio(); + await models.Client.updatePortfolio(options); const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `; const [salesPerson] = await models.Client.rawSql(portfolioQuery, null, options); @@ -30,21 +48,21 @@ describe('Client updatePortfolio', () => { }); it('should keep the same portfolioWeight when a salesperson is unassigned of a client', async() => { - pending('task 3817'); + const clientId = 1107; const salesPersonId = 19; const tx = await models.Client.beginTransaction({}); try { const options = {transaction: tx}; - const expectedResult = 34.40; - await models.Client.rawSql(`UPDATE vn.client SET salesPersonFk = NULL WHERE id = ${clientId}; `); + const client = await models.Client.findById(clientId, null, options); + await client.updateAttribute('salesPersonFk', null, options); await models.Client.updatePortfolio(); const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `; - const [salesPerson] = await models.Client.rawSql(portfolioQuery, null, options); + const [salesPerson] = await models.Client.rawSql(portfolioQuery); expect(salesPerson.portfolioWeight).toEqual(expectedResult); diff --git a/modules/client/back/methods/client/updateFiscalData.js b/modules/client/back/methods/client/updateFiscalData.js index c2de8f9279..7ae842c6e0 100644 --- a/modules/client/back/methods/client/updateFiscalData.js +++ b/modules/client/back/methods/client/updateFiscalData.js @@ -125,10 +125,10 @@ module.exports = Self => { } try { - const isAdministrative = await models.Account.hasRole(userId, 'administrative', myOptions); + const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions); const client = await models.Client.findById(clientId, null, myOptions); - if (!isAdministrative && client.isTaxDataChecked) + if (!isSalesAssistant && client.isTaxDataChecked) throw new UserError(`Not enough privileges to edit a client with verified data`); // Sage data validation diff --git a/modules/client/back/methods/client/updatePortfolio.js b/modules/client/back/methods/client/updatePortfolio.js index 3d522f6c82..809a84636b 100644 --- a/modules/client/back/methods/client/updatePortfolio.js +++ b/modules/client/back/methods/client/updatePortfolio.js @@ -13,8 +13,13 @@ module.exports = function(Self) { } }); - Self.updatePortfolio = async() => { + Self.updatePortfolio = async options => { + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + query = `CALL bs.salesPerson_updatePortfolio()`; - return await Self.rawSql(query); + return Self.rawSql(query, null, myOptions); }; }; diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 5ed777ab52..90a9b9e23c 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -31,6 +31,7 @@ module.exports = Self => { require('../methods/client/createReceipt')(Self); require('../methods/client/updatePortfolio')(Self); require('../methods/client/checkDuplicated')(Self); + require('../methods/client/extendedListFilter')(Self); // Validations @@ -232,7 +233,6 @@ module.exports = Self => { const loopBackContext = LoopBackContext.getCurrentContext(); const userId = loopBackContext.active.accessToken.userId; - const isAdministrative = await models.Account.hasRole(userId, 'administrative', ctx.options); const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', ctx.options); const hasChanges = orgData && changes; @@ -245,7 +245,7 @@ module.exports = Self => { const sageTransactionType = hasChanges && (changes.sageTransactionTypeFk || orgData.sageTransactionTypeFk); const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType; - const cantEditVerifiedData = isTaxDataCheckedChanged && !isAdministrative; + const cantEditVerifiedData = isTaxDataCheckedChanged && !isSalesAssistant; const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !isSalesAssistant; if (cantEditVerifiedData || cantChangeSageData) diff --git a/modules/client/back/models/client.json b/modules/client/back/models/client.json index b9951e8bbc..1426152a4d 100644 --- a/modules/client/back/models/client.json +++ b/modules/client/back/models/client.json @@ -136,6 +136,9 @@ "mysql": { "columnName": "businessTypeFk" } + }, + "salesPersonFk": { + "type": "number" } }, "relations": { diff --git a/modules/client/front/extended-list/index.html b/modules/client/front/extended-list/index.html new file mode 100644 index 0000000000..b45a0bc5f4 --- /dev/null +++ b/modules/client/front/extended-list/index.html @@ -0,0 +1,319 @@ +<vn-crud-model + vn-id="model" + url="Clients/extendedListFilter" + limit="20"> +</vn-crud-model> +<vn-portal slot="topbar"> + <vn-searchbar + vn-focus + panel="vn-client-search-panel" + placeholder="Search client" + info="Search client by id or name" + auto-state="false" + model="model"> + </vn-searchbar> +</vn-portal> +<vn-card> + <smart-table + model="model" + view-config-id="clientsDetail" + options="$ctrl.smartTableOptions" + expr-builder="$ctrl.exprBuilder(param, value)"> + <slot-table> + <table> + <thead> + <tr> + <th></th> + <th field="id"> + <span translate>Identifier</span> + </th> + <th field="name"> + <span translate>Name</span> + </th> + <th field="socialName"> + <span translate>Social name</span> + </th> + <th field="fi"> + <span translate>Tax number</span> + </th> + <th field="salesPersonFk"> + <span translate>Salesperson</span> + </th> + <th field="credit"> + <span translate>Credit</span> + </th> + <th field="creditInsurance"> + <span translate>Credit insurance</span> + </th> + <th field="phone"> + <span translate>Phone</span> + </th> + <th field="mobile"> + <span translate>Mobile</span> + </th> + <th field="street"> + <span translate>Street</span> + </th> + <th field="countryFk"> + <span translate>Country</span> + </th> + <th field="provinceFk"> + <span translate>Province</span> + </th> + <th field="city"> + <span translate>City</span> + </th> + <th field="postcode"> + <span translate>Postcode</span> + </th> + <th field="email"> + <span translate>Email</span> + </th> + <th field="created"> + <span translate>Created</span> + </th> + <th field="businessTypeFk"> + <span translate>Business type</span> + </th> + <th field="payMethodFk"> + <span translate>Billing data</span> + </th> + <th field="sageTaxTypeFk"> + <span translate>Sage tax type</span> + </th> + <th field="sageTransactionTypeFk"> + <span translate>Sage tr. type</span> + </th> + <th field="isActive" centered> + <span translate>Active</span> + </th> + <th field="isVies" centered> + <span translate>Vies</span> + </th> + <th field="isTaxDataChecked" centered> + <span translate>Verified data</span> + </th> + <th field="isEqualizated" centered> + <span translate>Is equalizated</span> + </th> + <th field="isFreezed" centered> + <span translate>Freezed</span> + </th> + <th field="hasToInvoice" centered> + <span translate>Invoice</span> + </th> + <th field="hasToInvoiceByAddress" centered> + <span translate>Invoice by address</span> + </th> + <th field="isToBeMailed" centered> + <span translate>Mailing</span> + </th> + <th field="hasLcr" centered> + <span translate>Received LCR</span> + </th> + <th field="hasCoreVnl" centered> + <span translate>Received core VNL</span> + </th> + <th field="hasSepaVnl" centered> + <span translate>Received B2B VNL</span> + </th> + <th></th> + </tr> + </thead> + <tbody> + <tr ng-repeat="client in model.data" + vn-anchor="::{ + state: 'client.card.summary', + params: {id: client.id} + }"> + <td> + <vn-icon-button ng-show="::client.isActive == false" + vn-tooltip="Client inactive" + icon="icon-disabled"> + </vn-icon-button> + <vn-icon-button ng-show="::client.isActive && client.isFreezed == true" + vn-tooltip="Client frozen" + icon="icon-frozen"> + </vn-icon-button> + </td> + <td> + <span + vn-click-stop="clientDescriptor.show($event, client.id)" + class="link"> + {{::client.id}} + </span> + </td> + <td>{{::client.name}}</td> + <td>{{::client.socialName}}</td> + <td>{{::client.fi}}</td> + <td> + <span + vn-click-stop="workerDescriptor.show($event, client.salesPersonFk)" + ng-class="{'link': client.salesPersonFk}"> + {{::client.salesPerson | dashIfEmpty}} + </span> + </td> + <td>{{::client.credit}}</td> + <td>{{::client.creditInsurance | dashIfEmpty}}</td> + <td>{{::client.phone | dashIfEmpty}}</td> + <td>{{::client.mobile | dashIfEmpty}}</td> + <td>{{::client.street | dashIfEmpty}}</td> + <td>{{::client.country | dashIfEmpty}}</td> + <td>{{::client.province | dashIfEmpty}}</td> + <td>{{::client.city | dashIfEmpty}}</td> + <td>{{::client.postcode | dashIfEmpty}}</td> + <td>{{::client.email | dashIfEmpty}}</td> + <td>{{::client.created | date:'dd/MM/yyyy'}}</td> + <td>{{::client.businessType | dashIfEmpty}}</td> + <td>{{::client.payMethod | dashIfEmpty}}</td> + <td>{{::client.sageTaxType | dashIfEmpty}}</td> + <td>{{::client.sageTransactionType | dashIfEmpty}}</td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.isActive, + 'alert': !client.isActive, + }"> + {{ ::client.isActive ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.isVies, + 'alert': !client.isVies, + }"> + {{ ::client.isVies ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.isTaxDataChecked, + 'alert': !client.isTaxDataChecked, + }"> + {{ ::client.isTaxDataChecked ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.isEqualizated, + 'alert': !client.isEqualizated, + }"> + {{ ::client.isEqualizated ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.isFreezed, + 'alert': !client.isFreezed, + }"> + {{ ::client.isFreezed ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.hasToInvoice, + 'alert': !client.hasToInvoice, + }"> + {{ ::client.hasToInvoice ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.hasToInvoiceByAddress, + 'alert': !client.hasToInvoiceByAddress, + }"> + {{ ::client.hasToInvoiceByAddress ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.isToBeMailed, + 'alert': !client.isToBeMailed, + }"> + {{ ::client.isToBeMailed ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.hasLcr, + 'alert': !client.hasLcr, + }"> + {{ ::client.hasLcr ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.hasCoreVnl, + 'alert': !client.hasCoreVnl, + }"> + {{ ::client.hasCoreVnl ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td centered> + <vn-chip ng-class="::{ + 'success': client.hasSepaVnl, + 'alert': !client.hasSepaVnl, + }"> + {{ ::client.hasSepaVnl ? 'Yes' : 'No' | translate}} + </vn-chip> + </td> + <td shrink> + <vn-horizontal class="buttons"> + <vn-icon-button vn-anchor="{state: 'ticket.index', params: {q: {clientFk: client.id} } }" + vn-tooltip="Client tickets" + icon="icon-ticket"> + </vn-icon-button> + <vn-icon-button + vn-click-stop="$ctrl.preview(client)" + vn-tooltip="Preview" + icon="preview"> + </vn-icon-button> + </vn-horizontal> + </td> + </tr> + </tbody> + </table> + </slot-table> + </smart-table> +</vn-card> +<a ui-sref="client.create" vn-tooltip="New client" vn-bind="+" fixed-bottom-right> + <vn-float-button icon="add"></vn-float-button> +</a> +<vn-client-descriptor-popover + vn-id="client-descriptor"> +</vn-client-descriptor-popover> +<vn-worker-descriptor-popover + vn-id="worker-descriptor"> +</vn-worker-descriptor-popover> + +<vn-popup vn-id="preview"> + <vn-client-summary + client="$ctrl.clientSelected"> + </vn-client-summary> +</vn-popup> +<vn-contextmenu + vn-id="contextmenu" + targets="['smart-table']" + model="model" + expr-builder="$ctrl.exprBuilder(param, value)"> + <slot-menu> + <vn-item translate + ng-if="contextmenu.isFilterAllowed()" + ng-click="contextmenu.filterBySelection()"> + Filter by selection + </vn-item> + <vn-item translate + ng-if="contextmenu.isFilterAllowed()" + ng-click="contextmenu.excludeSelection()"> + Exclude selection + </vn-item> + <vn-item translate + ng-if="contextmenu.isFilterAllowed()" + ng-click="contextmenu.removeFilter()"> + Remove filter + </vn-item> + <vn-item translate + ng-click="contextmenu.removeAllFilters()"> + Remove all filters + </vn-item> + </slot-menu> +</vn-contextmenu> \ No newline at end of file diff --git a/modules/client/front/extended-list/index.js b/modules/client/front/extended-list/index.js new file mode 100644 index 0000000000..8eed48d01c --- /dev/null +++ b/modules/client/front/extended-list/index.js @@ -0,0 +1,184 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + + this.smartTableOptions = { + activeButtons: { + search: true, + shownColumns: true, + }, + columns: [ + { + field: 'socialName', + autocomplete: { + url: 'Clients', + showField: 'socialName', + valueField: 'socialName', + } + }, + { + field: 'created', + datepicker: true + }, + { + field: 'countryFk', + autocomplete: { + url: 'Countries', + showField: 'country', + } + }, + { + field: 'provinceFk', + autocomplete: { + url: 'Provinces' + } + }, + { + field: 'salesPersonFk', + autocomplete: { + url: 'Workers/activeWithInheritedRole', + where: `{role: 'salesPerson'}`, + searchFunction: '{firstName: $search}', + showField: 'nickname', + valueField: 'id', + } + }, + { + field: 'businessTypeFk', + autocomplete: { + url: 'BusinessTypes', + valueField: 'code', + showField: 'description', + } + }, + { + field: 'payMethodFk', + autocomplete: { + url: 'PayMethods', + } + }, + { + field: 'sageTaxTypeFk', + autocomplete: { + url: 'SageTaxTypes', + showField: 'vat', + } + }, + { + field: 'sageTransactionTypeFk', + autocomplete: { + url: 'SageTransactionTypes', + showField: 'transaction', + } + }, + { + field: 'isActive', + checkbox: true + }, + { + field: 'isVies', + checkbox: true + }, + { + field: 'isTaxDataChecked', + checkbox: true + }, + { + field: 'isEqualizated', + checkbox: true + }, + { + field: 'isFreezed', + checkbox: true + }, + { + field: 'hasToInvoice', + checkbox: true + }, + { + field: 'hasToInvoiceByAddress', + checkbox: true + }, + { + field: 'isToBeMailed', + checkbox: true + }, + { + field: 'hasSepaVnl', + checkbox: true + }, + { + field: 'hasLcr', + checkbox: true + }, + { + field: 'hasCoreVnl', + checkbox: true + } + ] + }; + } + + exprBuilder(param, value) { + switch (param) { + case 'created': + return {'c.created': { + between: this.dateRange(value)} + }; + case 'id': + case 'name': + case 'socialName': + case 'fi': + case 'credit': + case 'creditInsurance': + case 'phone': + case 'mobile': + case 'street': + case 'city': + case 'postcode': + case 'email': + case 'isActive': + case 'isVies': + case 'isTaxDataChecked': + case 'isEqualizated': + case 'isFreezed': + case 'hasToInvoice': + case 'hasToInvoiceByAddress': + case 'isToBeMailed': + case 'hasSepaVnl': + case 'hasLcr': + case 'hasCoreVnl': + case 'countryFk': + case 'provinceFk': + case 'salesPersonFk': + case 'businessTypeFk': + case 'payMethodFk': + case 'sageTaxTypeFk': + case 'sageTransactionTypeFk': + return {[`c.${param}`]: value}; + } + } + + dateRange(value) { + const minHour = new Date(value); + minHour.setHours(0, 0, 0, 0); + const maxHour = new Date(value); + maxHour.setHours(23, 59, 59, 59); + + return [minHour, maxHour]; + } + + preview(client) { + this.clientSelected = client; + this.$.preview.show(); + } +} + +ngModule.vnComponent('vnClientExtendedList', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/client/front/extended-list/locale/es.yml b/modules/client/front/extended-list/locale/es.yml new file mode 100644 index 0000000000..ea56f3af27 --- /dev/null +++ b/modules/client/front/extended-list/locale/es.yml @@ -0,0 +1,3 @@ +Mailing: Env. emails +Sage tr. type: Tipo tr. sage +Yes: Sí \ No newline at end of file diff --git a/modules/client/front/extended-list/style.scss b/modules/client/front/extended-list/style.scss new file mode 100644 index 0000000000..7625b5d16e --- /dev/null +++ b/modules/client/front/extended-list/style.scss @@ -0,0 +1,6 @@ +@import "variables"; + +vn-chip.success, +vn-chip.alert { + color: $color-font-bg +} \ No newline at end of file diff --git a/modules/client/front/fiscal-data/index.html b/modules/client/front/fiscal-data/index.html index 2249127c52..1f35333276 100644 --- a/modules/client/front/fiscal-data/index.html +++ b/modules/client/front/fiscal-data/index.html @@ -182,7 +182,7 @@ vn-one label="Verified data" ng-model="$ctrl.client.isTaxDataChecked" - vn-acl="administrative"> + vn-acl="salesAssistant"> </vn-check> </vn-horizontal> </vn-card> diff --git a/modules/client/front/index.js b/modules/client/front/index.js index ea732beea1..a5782c7892 100644 --- a/modules/client/front/index.js +++ b/modules/client/front/index.js @@ -47,3 +47,4 @@ import './consumption-search-panel'; import './defaulter'; import './notification'; import './unpaid'; +import './extended-list'; diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml index 4eb99318c4..de4b91e0b1 100644 --- a/modules/client/front/locale/es.yml +++ b/modules/client/front/locale/es.yml @@ -33,6 +33,7 @@ Search client by id or name: Buscar clientes por identificador o nombre # Sections Clients: Clientes +Extended list: Listado extendido Defaulter: Morosos New client: Nuevo cliente Fiscal data: Datos fiscales diff --git a/modules/client/front/routes.json b/modules/client/front/routes.json index 293243470d..6616443bbf 100644 --- a/modules/client/front/routes.json +++ b/modules/client/front/routes.json @@ -7,6 +7,7 @@ "menus": { "main": [ {"state": "client.index", "icon": "person"}, + {"state": "client.extendedList", "icon": "person"}, {"state": "client.notification", "icon": "campaign"}, {"state": "client.defaulter", "icon": "icon-defaulter"} ], @@ -381,6 +382,12 @@ "component": "vn-client-unpaid", "acl": ["administrative"], "description": "Unpaid" + }, + { + "url": "/extended-list", + "state": "client.extendedList", + "component": "vn-client-extended-list", + "description": "Extended list" } ] } diff --git a/modules/client/front/search-panel/locale/es.yml b/modules/client/front/search-panel/locale/es.yml index 93d2faf538..b0d0649c8c 100644 --- a/modules/client/front/search-panel/locale/es.yml +++ b/modules/client/front/search-panel/locale/es.yml @@ -1,7 +1,7 @@ Client id: Id cliente Tax number: NIF/CIF Name: Nombre -Social name: Razon social +Social name: Razón social Town/City: Ciudad Postcode: Código postal Email: E-mail diff --git a/modules/mdb/back/methods/mdbVersion/upload.js b/modules/mdb/back/methods/mdbVersion/upload.js new file mode 100644 index 0000000000..3d54c02500 --- /dev/null +++ b/modules/mdb/back/methods/mdbVersion/upload.js @@ -0,0 +1,121 @@ +const fs = require('fs-extra'); +const path = require('path'); +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('upload', { + description: 'Upload and attach a access file', + accepts: [ + { + arg: 'appName', + type: 'string', + required: true, + description: 'The app name' + }, + { + arg: 'newVersion', + type: 'number', + required: true, + description: `The new version number` + }, + { + arg: 'branch', + type: 'string', + required: true, + description: `The branch name` + } + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/upload`, + verb: 'POST' + } + }); + + Self.upload = async(ctx, appName, newVersion, branch, options) => { + const models = Self.app.models; + const myOptions = {}; + + const TempContainer = models.TempContainer; + const AccessContainer = models.AccessContainer; + const fileOptions = {}; + + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + let srcFile; + try { + const tempContainer = await TempContainer.container('access'); + const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions); + const files = Object.values(uploaded.files).map(file => { + return file[0]; + }); + const uploadedFile = files[0]; + + const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name); + srcFile = path.join(file.client.root, file.container, file.name); + + const accessContainer = await AccessContainer.container('.archive'); + const destinationFile = path.join( + accessContainer.client.root, accessContainer.name, appName, `${newVersion}.7z`); + + if (process.env.NODE_ENV == 'test') + await fs.unlink(srcFile); + else { + await fs.move(srcFile, destinationFile, { + overwrite: true + }); + await fs.chmod(destinationFile, 0o644); + + const existBranch = await models.MdbBranch.findOne({ + where: {name: branch} + }); + + if (!existBranch) + throw new UserError('Not exist this branch'); + + const branchPath = path.join(accessContainer.client.root, 'branches', branch); + await fs.mkdir(branchPath, {recursive: true}); + + const destinationBranch = path.join(branchPath, `${appName}.7z`); + const destinationRoot = path.join(accessContainer.client.root, `${appName}.7z`); + try { + await fs.unlink(destinationBranch); + } catch (e) {} + await fs.symlink(destinationFile, destinationBranch); + + if (branch == 'master') { + try { + await fs.unlink(destinationRoot); + } catch (e) {} + await fs.symlink(destinationFile, destinationRoot); + } + } + + await models.MdbVersion.upsert({ + app: appName, + branchFk: branch, + version: newVersion + }); + + if (tx) await tx.commit(); + } catch (e) { + if (tx) await tx.rollback(); + + if (fs.existsSync(srcFile)) + await fs.unlink(srcFile); + + throw e; + } + }; +}; diff --git a/modules/mdb/back/model-config.json b/modules/mdb/back/model-config.json new file mode 100644 index 0000000000..d5be8de879 --- /dev/null +++ b/modules/mdb/back/model-config.json @@ -0,0 +1,11 @@ +{ + "MdbBranch": { + "dataSource": "vn" + }, + "MdbVersion": { + "dataSource": "vn" + }, + "AccessContainer": { + "dataSource": "accessStorage" + } +} diff --git a/modules/mdb/back/models/mdb-container.json b/modules/mdb/back/models/mdb-container.json new file mode 100644 index 0000000000..a927b30f1f --- /dev/null +++ b/modules/mdb/back/models/mdb-container.json @@ -0,0 +1,10 @@ +{ + "name": "AccessContainer", + "base": "Container", + "acls": [{ + "accessType": "*", + "principalType": "ROLE", + "principalId": "developer", + "permission": "ALLOW" + }] +} \ No newline at end of file diff --git a/modules/mdb/back/models/mdbBranch.json b/modules/mdb/back/models/mdbBranch.json new file mode 100644 index 0000000000..486dfaf25e --- /dev/null +++ b/modules/mdb/back/models/mdbBranch.json @@ -0,0 +1,16 @@ +{ + "name": "MdbBranch", + "base": "VnModel", + "options": { + "mysql": { + "table": "mdbBranch" + } + }, + "properties": { + "name": { + "id": true, + "type": "string", + "description": "Identifier" + } + } +} \ No newline at end of file diff --git a/modules/mdb/back/models/mdbVersion.js b/modules/mdb/back/models/mdbVersion.js new file mode 100644 index 0000000000..b36ee2a601 --- /dev/null +++ b/modules/mdb/back/models/mdbVersion.js @@ -0,0 +1,3 @@ +module.exports = Self => { + require('../methods/mdbVersion/upload')(Self); +}; diff --git a/modules/mdb/back/models/mdbVersion.json b/modules/mdb/back/models/mdbVersion.json new file mode 100644 index 0000000000..02635ff8a1 --- /dev/null +++ b/modules/mdb/back/models/mdbVersion.json @@ -0,0 +1,26 @@ +{ + "name": "MdbVersion", + "base": "VnModel", + "options": { + "mysql": { + "table": "mdbVersion" + } + }, + "properties": { + "app": { + "type": "string", + "description": "The app name", + "id": true + }, + "version": { + "type": "number" + } + }, + "relations": { + "branch": { + "type": "belongsTo", + "model": "MdbBranch", + "foreignKey": "branchFk" + } + } +} \ No newline at end of file diff --git a/modules/monitor/back/methods/sales-monitor/salesFilter.js b/modules/monitor/back/methods/sales-monitor/salesFilter.js index 4521b23515..9b6030e9f1 100644 --- a/modules/monitor/back/methods/sales-monitor/salesFilter.js +++ b/modules/monitor/back/methods/sales-monitor/salesFilter.js @@ -304,7 +304,8 @@ module.exports = Self => { {'tp.hasTicketRequest': true}, {'tp.hasComponentLack': true}, {'tp.isTaxDataChecked': false}, - {'tp.itemShortage': {neq: null}} + {'tp.itemShortage': {neq: null}}, + {'tp.isTooLittle': true} ]}; } else if (hasProblems === false) { whereProblems = {and: [ @@ -313,7 +314,8 @@ module.exports = Self => { {'tp.hasTicketRequest': false}, {'tp.hasComponentLack': false}, {'tp.isTaxDataChecked': true}, - {'tp.itemShortage': null} + {'tp.itemShortage': null}, + {'tp.isTooLittle': false} ]}; } diff --git a/modules/monitor/front/index/tickets/index.js b/modules/monitor/front/index/tickets/index.js index 0770d8634b..91d9079d8a 100644 --- a/modules/monitor/front/index/tickets/index.js +++ b/modules/monitor/front/index/tickets/index.js @@ -53,7 +53,7 @@ export default class Controller extends Section { }, { field: 'shippedDate', - searchable: false + datepicker: true }, { field: 'theoreticalHour', diff --git a/modules/ticket/back/methods/expedition/filter.js b/modules/ticket/back/methods/expedition/filter.js index 538e199388..723d7c8443 100644 --- a/modules/ticket/back/methods/expedition/filter.js +++ b/modules/ticket/back/methods/expedition/filter.js @@ -36,7 +36,6 @@ module.exports = Self => { e.workerFk, i1.name packageItemName, e.counter, - e.checked, i2.name freightItemName, e.itemFk, u.name userName, diff --git a/storage/access/.keep b/storage/access/.keep new file mode 100644 index 0000000000..8e25568966 --- /dev/null +++ b/storage/access/.keep @@ -0,0 +1 @@ +Forces tmp folder creation! \ No newline at end of file