Merge branch 'dev' into #288-traducir-state-i18n
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Pau 2022-11-16 12:54:47 +00:00
commit 8059e54a26
192 changed files with 45012 additions and 4631 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@ coverage
node_modules
dist
storage
.idea
npm-debug.log
.eslintcache
datasources.*.json

View File

@ -1,32 +1,43 @@
FROM debian:stretch-slim
FROM debian:bullseye-slim
ENV TZ Europe/Madrid
ARG DEBIAN_FRONTEND=noninteractive
# NodeJs
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
ca-certificates \
gnupg2 \
libfontconfig lftp \
&& apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
&& curl -sL https://deb.nodesource.com/setup_14.x | bash - \
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g npm@8.19.2
# Puppeteer
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
nodejs \
&& apt-get purge -y --auto-remove \
gnupg2 \
libfontconfig lftp xvfb gconf-service libasound2 libatk1.0-0 libc6 \
libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \
libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 \
libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
&& rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2
# Salix
WORKDIR /salix
COPY print/package.json print/package-lock.json print/
RUN npm --prefix ./print install --omit=dev ./print
COPY package.json package-lock.json ./
COPY loopback/package.json loopback/
COPY print/package.json print/
RUN npm install --only=prod
RUN npm --prefix ./print install --only=prod ./print
RUN npm install --omit=dev
COPY loopback loopback
COPY back back

15
Jenkinsfile vendored
View File

@ -1,5 +1,4 @@
#!/usr/bin/env groovy
pipeline {
agent any
options {
@ -62,13 +61,13 @@ pipeline {
}
}
}
// stage('Backend') {
// steps {
// nodejs('node-v14') {
// sh 'npm run test:back:ci'
// }
// }
// }
stage('Backend') {
steps {
nodejs('node-v14') {
sh 'npm run test:back:ci'
}
}
}
}
}
stage('Build') {

View File

@ -1,4 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceIn', 'invoiceInPdf', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', 'invoiceInEmail', 'WRITE', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', 'invoiceInEmail', 'WRITE', 'ALLOW', 'ROLE', 'administrative');

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`workerTimeControlMail` CHANGE emailResponse reason text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL NULL;

View File

@ -0,0 +1,7 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceOut', 'clientsToInvoice', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceOut', 'invoiceClient', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');

View File

@ -0,0 +1 @@
DROP TABLE `vn`.`invoiceOutQueue`;

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Sale', 'editTracked', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Sale', 'editFloramondo', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant');

View File

@ -918,7 +918,7 @@ INSERT INTO `vn`.`expeditionStateType`(`id`, `description`, `code`)
(3, 'Perdida', 'LOST');
INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `freightItemFk`, `created`, `itemFk`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`)
INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `freightItemFk`, `created`, `itemFk`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`, `hostFk`)
VALUES
(1, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 15, 1, 18, 'UR9000006041', 94, 1, 'pc1'),
(2, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 16, 2, 18, 'UR9000006041', 94, 1, NULL),
@ -1149,7 +1149,8 @@ INSERT INTO `vn`.`saleTracking`(`saleFk`, `isChecked`, `created`, `originalQuant
(1, 0, util.VN_CURDATE(), 5, 55, 3, 1, 14),
(1, 1, util.VN_CURDATE(), 5, 54, 3, 2, 8),
(2, 1, util.VN_CURDATE(), 10, 40, 4, 3, 8),
(3, 1, util.VN_CURDATE(), 2, 40, 4, 4, 8);
(3, 1, util.VN_CURDATE(), 2, 40, 4, 4, 8),
(31, 1, util.VN_CURDATE(), -5, 40, 4, 5, 8);
INSERT INTO `vn`.`itemBarcode`(`id`, `itemFk`, `code`)
VALUES
@ -1356,7 +1357,8 @@ INSERT INTO `vn`.`ticketWeekly`(`ticketFk`, `weekDay`)
(2, 1),
(3, 2),
(4, 4),
(5, 6);
(5, 6),
(15, 6);
INSERT INTO `vn`.`travel`(`id`,`shipped`, `landed`, `warehouseInFk`, `warehouseOutFk`, `agencyModeFk`, `m3`, `kg`,`ref`, `totalEntries`, `cargoSupplierFk`)
VALUES
@ -2267,12 +2269,16 @@ INSERT INTO `vn`.`zoneEvent`(`zoneFk`, `type`, `started`, `ended`)
VALUES
(9, 'range', DATE_ADD(util.VN_CURDATE(), INTERVAL -1 YEAR), DATE_ADD(util.VN_CURDATE(), INTERVAL +1 YEAR));
INSERT INTO `vn`.`workerTimeControl`(`userFk`, `timed`, `manual`, `direction`)
INSERT INTO `vn`.`workerTimeControl`(`userFk`, `timed`, `manual`, `direction`, `isSendMail`)
VALUES
(1106, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in'),
(1106, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle'),
(1106, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle'),
(1106, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out');
(1106, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in', 0),
(1106, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle', 0),
(1106, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle', 0),
(1106, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out', 0),
(1107, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in', 1),
(1107, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle', 1),
(1107, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle', 1),
(1107, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out', 1);
INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `code`)
VALUES
@ -2708,6 +2714,10 @@ INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `lev
VALUES
(9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `vn`.`saleCloned` (`saleClonedFk`, `saleOriginalFk`)
VALUES
(29, 25);
UPDATE `account`.`user`
SET `hasGrant` = 1
WHERE `id` = 66;

View File

@ -969,6 +969,7 @@ export default {
manualInvoiceTaxArea: 'vn-autocomplete[ng-model="$ctrl.invoice.taxArea"]',
saveInvoice: 'button[response="accept"]',
globalInvoiceForm: '.vn-invoice-out-global-invoicing',
globalInvoiceClientsRange: 'vn-radio[val="clientsRange"]',
globalInvoiceDate: '[ng-model="$ctrl.invoice.invoiceDate"]',
globalInvoiceFromClient: '[ng-model="$ctrl.invoice.fromClientId"]',
globalInvoiceToClient: '[ng-model="$ctrl.invoice.toClientId"]',

View File

@ -104,7 +104,6 @@ describe('Ticket Edit basic data path', () => {
await page.waitToClick(selectors.ticketBasicData.nextStepButton);
await page.waitToClick(selectors.ticketBasicData.withoutNegatives);
await page.waitToClick(selectors.ticketBasicData.finalizeButton);
await page.waitForState('ticket.card.summary');

View File

@ -19,7 +19,7 @@ describe('Ticket descriptor path', () => {
it('should count the amount of tickets in the turns section', async() => {
const result = await page.countElement(selectors.ticketsIndex.weeklyTicket);
expect(result).toEqual(5);
expect(result).toEqual(6);
});
it('should go back to the ticket index then search and access a ticket summary', async() => {
@ -104,7 +104,7 @@ describe('Ticket descriptor path', () => {
await page.doSearch();
const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult);
expect(nResults).toEqual(5);
expect(nResults).toEqual(6);
});
it('should update the agency then remove it afterwards', async() => {

View File

@ -33,6 +33,7 @@ describe('InvoiceOut global invoice path', () => {
it('should create a global invoice for charles xavier today', async() => {
await page.pickDate(selectors.invoiceOutIndex.globalInvoiceDate);
await page.waitToClick(selectors.invoiceOutIndex.globalInvoiceClientsRange);
await page.autocompleteSearch(selectors.invoiceOutIndex.globalInvoiceFromClient, 'Petter Parker');
await page.autocompleteSearch(selectors.invoiceOutIndex.globalInvoiceToClient, 'Petter Parker');
await page.waitToClick(selectors.invoiceOutIndex.saveInvoice);
@ -48,4 +49,15 @@ describe('InvoiceOut global invoice path', () => {
expect(currentInvoices).toBeGreaterThan(invoicesBefore);
});
it('should create a global invoice for all clients today', async() => {
await page.waitToClick(selectors.invoiceOutIndex.createInvoice);
await page.waitToClick(selectors.invoiceOutIndex.createGlobalInvoice);
await page.waitForSelector(selectors.invoiceOutIndex.globalInvoiceForm);
await page.pickDate(selectors.invoiceOutIndex.globalInvoiceDate);
await page.waitToClick(selectors.invoiceOutIndex.saveInvoice);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
});

View File

@ -24,10 +24,11 @@
}
},
"node_modules/@uirouter/angularjs": {
"version": "1.0.29",
"license": "MIT",
"version": "1.0.30",
"resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.30.tgz",
"integrity": "sha512-qkc3RFZc91S5K0gc/QVAXc9LGDPXjR04vDgG/11j8+yyZEuQojXxKxdLhKIepiPzqLmGRVqzBmBc27gtqaEeZg==",
"dependencies": {
"@uirouter/core": "6.0.7"
"@uirouter/core": "6.0.8"
},
"engines": {
"node": ">=4.0.0"
@ -37,15 +38,18 @@
}
},
"node_modules/@uirouter/core": {
"version": "6.0.7",
"license": "MIT",
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.8.tgz",
"integrity": "sha512-Gc/BAW47i4L54p8dqYCJJZuv2s3tqlXQ0fvl6Zp2xrblELPVfxmjnc0eurx3XwfQdaqm3T6uls6tQKkof/4QMw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/angular": {
"version": "1.8.2",
"license": "MIT"
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==",
"deprecated": "For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward."
},
"node_modules/angular-animate": {
"version": "1.8.2",
@ -62,8 +66,9 @@
}
},
"node_modules/angular-translate": {
"version": "2.18.4",
"license": "MIT",
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.19.0.tgz",
"integrity": "sha512-Z/Fip5uUT2N85dPQ0sMEe1JdF5AehcDe4tg/9mWXNDVU531emHCg53ZND9Oe0dyNiGX5rWcJKmsL1Fujus1vGQ==",
"dependencies": {
"angular": "^1.8.0"
},
@ -72,10 +77,11 @@
}
},
"node_modules/angular-translate-loader-partial": {
"version": "2.18.4",
"license": "MIT",
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/angular-translate-loader-partial/-/angular-translate-loader-partial-2.19.0.tgz",
"integrity": "sha512-NnMw13LMV4bPQmJK7/pZOZAnPxe0M5OtUHchADs5Gye7V7feonuEnrZ8e1CKhBlv9a7IQyWoqcBa4Lnhg8gk5w==",
"dependencies": {
"angular-translate": "~2.18.4"
"angular-translate": "~2.19.0"
}
},
"node_modules/argparse": {
@ -119,8 +125,9 @@
}
},
"node_modules/moment": {
"version": "2.29.1",
"license": "MIT",
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
@ -164,16 +171,22 @@
},
"dependencies": {
"@uirouter/angularjs": {
"version": "1.0.29",
"version": "1.0.30",
"resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.30.tgz",
"integrity": "sha512-qkc3RFZc91S5K0gc/QVAXc9LGDPXjR04vDgG/11j8+yyZEuQojXxKxdLhKIepiPzqLmGRVqzBmBc27gtqaEeZg==",
"requires": {
"@uirouter/core": "6.0.7"
"@uirouter/core": "6.0.8"
}
},
"@uirouter/core": {
"version": "6.0.7"
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.8.tgz",
"integrity": "sha512-Gc/BAW47i4L54p8dqYCJJZuv2s3tqlXQ0fvl6Zp2xrblELPVfxmjnc0eurx3XwfQdaqm3T6uls6tQKkof/4QMw=="
},
"angular": {
"version": "1.8.2"
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw=="
},
"angular-animate": {
"version": "1.8.2"
@ -185,15 +198,19 @@
}
},
"angular-translate": {
"version": "2.18.4",
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.19.0.tgz",
"integrity": "sha512-Z/Fip5uUT2N85dPQ0sMEe1JdF5AehcDe4tg/9mWXNDVU531emHCg53ZND9Oe0dyNiGX5rWcJKmsL1Fujus1vGQ==",
"requires": {
"angular": "^1.8.0"
}
},
"angular-translate-loader-partial": {
"version": "2.18.4",
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/angular-translate-loader-partial/-/angular-translate-loader-partial-2.19.0.tgz",
"integrity": "sha512-NnMw13LMV4bPQmJK7/pZOZAnPxe0M5OtUHchADs5Gye7V7feonuEnrZ8e1CKhBlv9a7IQyWoqcBa4Lnhg8gk5w==",
"requires": {
"angular-translate": "~2.18.4"
"angular-translate": "~2.19.0"
}
},
"argparse": {
@ -222,7 +239,9 @@
}
},
"moment": {
"version": "2.29.1"
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"oclazyload": {
"version": "0.6.3"

View File

@ -136,5 +136,6 @@
"Not enough privileges to edit a client": "Not enough privileges to edit a client",
"Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*",
"You don't have grant privilege": "You don't have grant privilege",
"You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user"
"You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user",
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production"
}

View File

@ -153,6 +153,7 @@
"Email already exists": "Email already exists",
"User already exists": "User already exists",
"Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral",
"Record of hours week": "Registro de horas semana {{week}} año {{year}} ",
"Created absence": "El empleado <strong>{{author}}</strong> ha añadido una ausencia de tipo '{{absenceType}}' a <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> para el día {{dated}}.",
"Deleted absence": "El empleado <strong>{{author}}</strong> ha eliminado una ausencia de tipo '{{absenceType}}' a <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> del día {{dated}}.",
"I have deleted the ticket id": "He eliminado el ticket id [{{id}}]({{{url}}})",
@ -240,5 +241,7 @@
"Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*",
"You don't have grant privilege": "No tienes privilegios para dar privilegios",
"You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario",
"Already has this status": "Ya tiene este estado",
"There aren't records for this week": "No existen registros para esta semana",
"Empty data source": "Origen de datos vacio"
}

View File

@ -0,0 +1,22 @@
module.exports = function(app) {
app.models.ACL.checkAccessAcl = async(ctx, modelId, property, accessType = '*') => {
const models = app.models;
const context = {
accessToken: ctx.req.accessToken,
model: models[modelId],
property: property,
modelId: modelId,
accessType: accessType,
sharedMethod: {
name: property,
aliases: [],
sharedClass: true
}
};
const acl = await models.ACL.checkAccessForContext(context);
return acl.permission == 'ALLOW';
};
};

View File

@ -1,6 +1,6 @@
module.exports = Self => {
Self.remoteMethodCtx('globalInvoicing', {
description: 'Make a global invoice',
Self.remoteMethodCtx('clientsToInvoice', {
description: 'Get the clients to make global invoicing',
accessType: 'WRITE',
accepts: [
{
@ -29,19 +29,22 @@ module.exports = Self => {
description: 'The company id to invoice'
}
],
returns: {
type: 'object',
root: true
returns: [{
arg: 'clientsAndAddresses',
type: ['object']
},
{
arg: 'invoice',
type: 'object'
}],
http: {
path: '/globalInvoicing',
path: '/clientsToInvoice',
verb: 'POST'
}
});
Self.globalInvoicing = async(ctx, options) => {
Self.clientsToInvoice = async(ctx, options) => {
const args = ctx.args;
let tx;
const myOptions = {};
@ -53,8 +56,6 @@ module.exports = Self => {
myOptions.transaction = tx;
}
const invoicesIds = [];
const failedClients = [];
let query;
try {
query = `
@ -78,6 +79,9 @@ module.exports = Self => {
const minShipped = new Date();
minShipped.setFullYear(minShipped.getFullYear() - 1);
minShipped.setMonth(1);
minShipped.setDate(1);
minShipped.setHours(0, 0, 0, 0);
// Packaging liquidation
const vIsAllInvoiceable = false;
@ -93,110 +97,51 @@ module.exports = Self => {
const invoiceableClients = await getInvoiceableClients(ctx, myOptions);
if (!invoiceableClients.length) return;
if (!invoiceableClients) return;
for (let client of invoiceableClients) {
try {
if (client.hasToInvoiceByAddress) {
await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [
minShipped,
args.maxShipped,
client.addressFk,
args.companyFk
], myOptions);
} else {
await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [
args.maxShipped,
client.id,
args.companyFk
], myOptions);
const clientsAndAddresses = invoiceableClients.map(invoiceableClient => {
return {
clientId: invoiceableClient.id,
addressId: invoiceableClient.addressFk
};
}
// Make invoice
const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions);
// Validates ticket nagative base
const hasAnyNegativeBase = await getNegativeBase(myOptions);
if (hasAnyNegativeBase && isSpanishCompany)
continue;
query = `SELECT invoiceSerial(?, ?, ?) AS serial`;
const [invoiceSerial] = await Self.rawSql(query, [
client.id,
args.companyFk,
'G'
], myOptions);
const serialLetter = invoiceSerial.serial;
query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`;
await Self.rawSql(query, [
serialLetter,
args.invoiceDate
], myOptions);
const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions);
if (newInvoice.id) {
await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions);
query = `INSERT IGNORE INTO invoiceOutQueue(invoiceFk) VALUES(?)`;
await Self.rawSql(query, [newInvoice.id], myOptions);
invoicesIds.push(newInvoice.id);
}
} catch (e) {
failedClients.push({
id: client.id,
stacktrace: e
});
continue;
}
}
if (failedClients.length > 0)
await notifyFailures(ctx, failedClients, myOptions);
);
if (tx) await tx.commit();
return [
clientsAndAddresses,
{
invoiceDate: args.invoiceDate,
maxShipped: args.maxShipped,
fromClientId: args.fromClientId,
toClientId: args.toClientId,
companyFk: args.companyFk,
minShipped: minShipped
}
];
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
return invoicesIds;
};
async function getNegativeBase(options) {
const models = Self.app.models;
const query = 'SELECT hasAnyNegativeBase() AS base';
const [result] = await models.InvoiceOut.rawSql(query, null, options);
return result && result.base;
}
async function getIsSpanishCompany(companyId, options) {
const models = Self.app.models;
const query = `SELECT COUNT(*) AS total
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
WHERE s.id = ?`;
const [supplierCompany] = await models.InvoiceOut.rawSql(query, [
companyId
], options);
return supplierCompany && supplierCompany.total;
}
async function getClientsWithPackaging(ctx, options) {
const models = Self.app.models;
const args = ctx.args;
const query = `SELECT DISTINCT clientFk AS id
FROM ticket t
JOIN ticketPackaging tp ON t.id = tp.ticketFk
JOIN client c ON c.id = t.clientFk
WHERE t.shipped BETWEEN '2017-11-21' AND ?
AND t.clientFk BETWEEN ? AND ?`;
AND t.clientFk >= ?
AND (t.clientFk <= ? OR ? IS NULL)
AND c.isActive`;
return models.InvoiceOut.rawSql(query, [
args.maxShipped,
args.fromClientId,
args.toClientId,
args.toClientId
], options);
}
@ -225,42 +170,20 @@ module.exports = Self => {
LEFT JOIN ticketService ts ON ts.ticketFk = t.id
JOIN address a ON a.id = t.addressFk
JOIN client c ON c.id = t.clientFk
WHERE ISNULL(t.refFk) AND c.id BETWEEN ? AND ?
WHERE ISNULL(t.refFk) AND c.id >= ?
AND (t.clientFk <= ? OR ? IS NULL)
AND t.shipped BETWEEN ? AND util.dayEnd(?)
AND t.companyFk = ? AND c.hasToInvoice
AND c.isTaxDataChecked
AND c.isTaxDataChecked AND c.isActive
GROUP BY c.id, IF(c.hasToInvoiceByAddress,a.id,TRUE) HAVING sumAmount > 0`;
return models.InvoiceOut.rawSql(query, [
args.fromClientId,
args.toClientId,
args.toClientId,
minShipped,
args.maxShipped,
args.companyFk
], options);
}
async function notifyFailures(ctx, failedClients, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const $t = ctx.req.__; // $translate
const worker = await models.EmailUser.findById(userId, null, options);
const subject = $t('Global invoicing failed');
let body = $t(`Wasn't able to invoice the following clients`) + ':<br/><br/>';
for (client of failedClients) {
body += `ID: <strong>${client.id}</strong>
<br/> <strong>${client.stacktrace}</strong><br/><br/>`;
}
await Self.rawSql(`
INSERT INTO vn.mail (sender, replyTo, sent, subject, body)
VALUES (?, ?, FALSE, ?, ?)`, [
worker.email,
worker.email,
subject,
body
], options);
}
};

View File

@ -0,0 +1,190 @@
module.exports = Self => {
Self.remoteMethodCtx('invoiceClient', {
description: 'Make a invoice of a client',
accessType: 'WRITE',
accepts: [{
arg: 'clientId',
type: 'number',
description: 'The client id to invoice',
required: true
},
{
arg: 'addressId',
type: 'number',
description: 'The address id to invoice',
required: true
},
{
arg: 'invoiceDate',
type: 'date',
description: 'The invoice date',
required: true
},
{
arg: 'maxShipped',
type: 'date',
description: 'The maximum shipped date',
required: true
},
{
arg: 'companyFk',
type: 'number',
description: 'The company id to invoice',
required: true
},
{
arg: 'minShipped',
type: 'date',
description: 'The minium shupped date',
required: true
}],
returns: {
type: 'object',
root: true
},
http: {
path: '/invoiceClient',
verb: 'POST'
}
});
Self.invoiceClient = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
let invoiceId;
let invoiceOut;
try {
const client = await models.Client.findById(args.clientId, {
fields: ['id', 'hasToInvoiceByAddress']
}, myOptions);
try {
if (client.hasToInvoiceByAddress) {
await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [
args.minShipped,
args.maxShipped,
args.addressId,
args.companyFk
], myOptions);
} else {
await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [
args.maxShipped,
client.id,
args.companyFk
], myOptions);
}
// Make invoice
const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions);
// Validates ticket nagative base
const hasAnyNegativeBase = await getNegativeBase(myOptions);
if (hasAnyNegativeBase && isSpanishCompany)
return tx.rollback();
query = `SELECT invoiceSerial(?, ?, ?) AS serial`;
const [invoiceSerial] = await Self.rawSql(query, [
client.id,
args.companyFk,
'G'
], myOptions);
const serialLetter = invoiceSerial.serial;
query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`;
await Self.rawSql(query, [
serialLetter,
args.invoiceDate
], myOptions);
const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions);
if (newInvoice.id) {
await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions);
invoiceId = newInvoice.id;
}
} catch (e) {
const failedClient = {
id: client.id,
stacktrace: e
};
await notifyFailures(ctx, failedClient, myOptions);
}
invoiceOut = await models.InvoiceOut.findById(invoiceId, {
include: {
relation: 'client'
}
}, myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
ctx.args = {
reference: invoiceOut.ref,
recipientId: invoiceOut.clientFk,
recipient: invoiceOut.client().email
};
try {
await models.InvoiceOut.invoiceEmail(ctx);
} catch (err) {}
return invoiceId;
};
async function getNegativeBase(options) {
const models = Self.app.models;
const query = 'SELECT hasAnyNegativeBase() AS base';
const [result] = await models.InvoiceOut.rawSql(query, null, options);
return result && result.base;
}
async function getIsSpanishCompany(companyId, options) {
const models = Self.app.models;
const query = `SELECT COUNT(*) AS total
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
WHERE s.id = ?`;
const [supplierCompany] = await models.InvoiceOut.rawSql(query, [
companyId
], options);
return supplierCompany && supplierCompany.total;
}
async function notifyFailures(ctx, failedClient, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const $t = ctx.req.__; // $translate
const worker = await models.EmailUser.findById(userId, null, options);
const subject = $t('Global invoicing failed');
let body = $t(`Wasn't able to invoice the following clients`) + ':<br/><br/>';
body += `ID: <strong>${failedClient.id}</strong>
<br/> <strong>${failedClient.stacktrace}</strong><br/><br/>`;
await Self.rawSql(`
INSERT INTO vn.mail (sender, replyTo, sent, subject, body)
VALUES (?, ?, FALSE, ?, ?)`, [
worker.email,
worker.email,
subject,
body
], options);
}
};

View File

@ -40,19 +40,40 @@ module.exports = Self => {
}
});
Self.invoiceEmail = async ctx => {
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
}
});
delete args.ctx;
for (const param in args)
params[param] = args[param];
const email = new Email('invoice', params);
return email.send();
const [stream, ...headers] = await Self.download(ctx, invoiceOut.id);
const name = headers[1];
const fileName = name.replace(/filename="(.*)"/gm, '$1');
const mailOptions = {
overrideAttachments: true,
attachments: [
{
filename: fileName,
content: stream
}
]
};
return email.send(mailOptions);
};
};

View File

@ -1,133 +0,0 @@
const {Email, Report, storage} = require('vn-print');
module.exports = Self => {
Self.remoteMethod('sendQueued', {
description: 'Send all queued invoices',
accessType: 'WRITE',
accepts: [],
returns: {
type: 'object',
root: true
},
http: {
path: '/send-queued',
verb: 'POST'
}
});
Self.sendQueued = async() => {
const invoices = await Self.rawSql(`
SELECT
io.id,
io.clientFk,
io.issued,
io.ref,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM invoiceOutQueue ioq
JOIN invoiceOut io ON io.id = ioq.invoiceFk
JOIN client c ON c.id = io.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE status = ''`);
let invoiceId;
for (const invoiceOut of invoices) {
try {
const tx = await Self.beginTransaction({});
const myOptions = {transaction: tx};
invoiceId = invoiceOut.id;
const args = {
reference: invoiceOut.ref,
recipientId: invoiceOut.clientFk,
recipient: invoiceOut.recipient,
replyTo: invoiceOut.salesPersonEmail
};
const invoiceReport = new Report('invoice', args);
const stream = 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`;
// Store invoice
storage.write(stream, {
type: 'invoice',
path: `${year}/${month}/${day}`,
fileName: fileName
});
await Self.rawSql(`
UPDATE invoiceOut
SET hasPdf = true
WHERE id = ?`,
[invoiceOut.id], myOptions);
const isToBeMailed = invoiceOut.recipient && invoiceOut.salesPersonFk && invoiceOut.isToBeMailed;
if (isToBeMailed) {
const mailOptions = {
overrideAttachments: true,
attachments: []
};
const invoiceAttachment = {
filename: fileName,
content: stream
};
if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
const exportation = new Report('exportation', args);
const stream = await exportation.toPdfStream();
const fileName = `CITES-${invoiceOut.ref}.pdf`;
mailOptions.attachments.push({
filename: fileName,
content: stream
});
}
mailOptions.attachments.push(invoiceAttachment);
const email = new Email('invoice', args);
await email.send(mailOptions);
}
// Update queue status
const date = new Date();
await Self.rawSql(`
UPDATE invoiceOutQueue
SET status = "printed",
printed = ?
WHERE invoiceFk = ?`,
[date, invoiceOut.id], myOptions);
await tx.commit();
} catch (error) {
await tx.rollback();
await Self.rawSql(`
UPDATE invoiceOutQueue
SET status = ?
WHERE invoiceFk = ?`,
[error.message, invoiceId]);
throw e;
}
}
return {
message: 'Success'
};
};
};

View File

@ -13,7 +13,7 @@ describe('InvoiceOut downloadZip()', () => {
};
it('should return part of link to dowloand the zip', async() => {
const tx = await models.Order.beginTransaction({});
const tx = await models.InvoiceOut.beginTransaction({});
try {
const options = {transaction: tx};
@ -30,7 +30,7 @@ describe('InvoiceOut downloadZip()', () => {
});
it('should return an error if the size of the files is too large', async() => {
const tx = await models.Order.beginTransaction({});
const tx = await models.InvoiceOut.beginTransaction({});
let error;
try {

View File

@ -1,43 +0,0 @@
const models = require('vn-loopback/server/server').models;
describe('InvoiceOut globalInvoicing()', () => {
const userId = 1;
const companyFk = 442;
const clientId = 1101;
const invoicedTicketId = 8;
const invoiceSerial = 'A';
const activeCtx = {
accessToken: {userId: userId},
__: value => {
return value;
}
};
const ctx = {req: activeCtx};
it('should make a global invoicing', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {
invoiceDate: new Date(),
maxShipped: new Date(),
fromClientId: clientId,
toClientId: 1106,
companyFk: companyFk
};
const result = await models.InvoiceOut.globalInvoicing(ctx, options);
const ticket = await models.Ticket.findById(invoicedTicketId, null, options);
expect(result.length).toBeGreaterThan(0);
expect(ticket.refFk).toContain(invoiceSerial);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,56 @@
const models = require('vn-loopback/server/server').models;
describe('InvoiceOut invoiceClient()', () => {
const userId = 1;
const clientId = 1101;
const addressId = 121;
const companyFk = 442;
const minShipped = new Date();
minShipped.setFullYear(minShipped.getFullYear() - 1);
minShipped.setMonth(1);
minShipped.setDate(1);
minShipped.setHours(0, 0, 0, 0);
const invoiceSerial = 'A';
const activeCtx = {
getLocale: () => {
return 'en';
},
accessToken: {userId: userId},
__: value => {
return value;
}
};
const ctx = {req: activeCtx};
it('should make a global invoicing', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
spyOn(models.InvoiceOut, 'invoiceEmail');
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {
clientId: clientId,
addressId: addressId,
invoiceDate: new Date(),
maxShipped: new Date(),
companyFk: companyFk,
minShipped: minShipped
};
const invoiceOutId = await models.InvoiceOut.invoiceClient(ctx, options);
const invoiceOut = await models.InvoiceOut.findById(invoiceOutId, null, options);
const [firstTicket] = await models.Ticket.find({
where: {refFk: invoiceOut.ref}
}, options);
expect(invoiceOutId).toBeGreaterThan(0);
expect(firstTicket.refFk).toContain(invoiceSerial);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -8,11 +8,11 @@ module.exports = Self => {
require('../methods/invoiceOut/book')(Self);
require('../methods/invoiceOut/createPdf')(Self);
require('../methods/invoiceOut/createManualInvoice')(Self);
require('../methods/invoiceOut/globalInvoicing')(Self);
require('../methods/invoiceOut/clientsToInvoice')(Self);
require('../methods/invoiceOut/invoiceClient')(Self);
require('../methods/invoiceOut/refund')(Self);
require('../methods/invoiceOut/invoiceEmail')(Self);
require('../methods/invoiceOut/exportationPdf')(Self);
require('../methods/invoiceOut/sendQueued')(Self);
require('../methods/invoiceOut/invoiceCsv')(Self);
require('../methods/invoiceOut/invoiceCsvEmail')(Self);
};

View File

@ -16,10 +16,21 @@
</vn-crud-model>
<div
class="progress vn-my-md"
ng-if="$ctrl.isInvoicing">
ng-if="$ctrl.packageInvoicing">
<vn-horizontal>
<vn-icon vn-none icon="warning"></vn-icon>
<span vn-none translate>Adding invoices to queue...</span>
<div>
{{'Calculating packages to invoice...' | translate}}
</div>
</vn-horizontal>
</div>
<div
class="progress vn-my-md"
ng-if="$ctrl.lastClientId">
<vn-horizontal>
<div>
{{'Id Client' | translate}}: {{$ctrl.currentClientId}}
{{'of' | translate}} {{::$ctrl.lastClientId}}
</div>
</vn-horizontal>
</div>
<vn-horizontal>
@ -35,10 +46,24 @@
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-radio
label="All clients"
val="allClients"
ng-model="$ctrl.clientsNumber"
ng-click="$ctrl.$onInit()">
</vn-radio>
<vn-radio
label="Clients range"
val="clientsRange"
ng-model="$ctrl.clientsNumber">
</vn-radio>
</vn-horizontal>
<vn-horizontal ng-show="$ctrl.clientsNumber == 'clientsRange'">
<vn-autocomplete
url="Clients"
label="From client"
search-function="{or: [{id: $search}, {name: {like: '%'+$search+'%'}}]}"
order="id"
show-field="name"
value-field="id"
ng-model="$ctrl.invoice.fromClientId">
@ -48,6 +73,7 @@
url="Clients"
label="To client"
search-function="{or: [{id: $search}, {name: {like: '%'+$search+'%'}}]}"
order="id"
show-field="name"
value-field="id"
ng-model="$ctrl.invoice.toClientId">
@ -66,5 +92,5 @@
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate vn-focus>Invoice</button>
<button vn-id="invoiceButton" response="accept" translate>Invoice</button>{{$ctrl.isInvoicing}}
</tpl-buttons>

View File

@ -5,11 +5,10 @@ import './style.scss';
class Controller extends Dialog {
constructor($element, $, $transclude) {
super($element, $, $transclude);
this.isInvoicing = false;
this.invoice = {
maxShipped: new Date()
};
this.clientsNumber = 'allClients';
}
$onInit() {
@ -46,6 +45,39 @@ class Controller extends Dialog {
this.invoice.companyFk = value;
}
restartValues() {
this.lastClientId = null;
this.$.invoiceButton.disabled = false;
}
cancelRequest() {
this.canceler = this.$q.defer();
return {timeout: this.canceler.promise};
}
invoiceOut(invoice, clientsAndAddresses) {
const [clientAndAddress] = clientsAndAddresses;
if (!clientAndAddress) return;
this.currentClientId = clientAndAddress.clientId;
const params = {
clientId: clientAndAddress.clientId,
addressId: clientAndAddress.addressId,
invoiceDate: invoice.invoiceDate,
maxShipped: invoice.maxShipped,
companyFk: invoice.companyFk,
minShipped: invoice.minShipped,
};
const options = this.cancelRequest();
return this.$http.post(`InvoiceOuts/invoiceClient`, params, options)
.then(() => {
clientsAndAddresses.shift();
return this.invoiceOut(invoice, clientsAndAddresses);
});
}
responseHandler(response) {
try {
if (response !== 'accept')
@ -57,14 +89,30 @@ class Controller extends Dialog {
if (!this.invoice.fromClientId || !this.invoice.toClientId)
throw new Error('Choose a valid clients range');
this.isInvoicing = true;
return this.$http.post(`InvoiceOuts/globalInvoicing`, this.invoice)
this.on('close', () => {
if (this.canceler) this.canceler.resolve();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
this.$.invoiceButton.disabled = true;
this.packageInvoicing = true;
const options = this.cancelRequest();
this.$http.post(`InvoiceOuts/clientsToInvoice`, this.invoice, options)
.then(res => {
this.packageInvoicing = false;
const invoice = res.data.invoice;
const clientsAndAddresses = res.data.clientsAndAddresses;
if (!clientsAndAddresses.length) return super.responseHandler(response);
this.lastClientId = clientsAndAddresses[clientsAndAddresses.length - 1].clientId;
return this.invoiceOut(invoice, clientsAndAddresses);
})
.then(() => super.responseHandler(response))
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
.finally(() => this.isInvoicing = false);
.finally(() => this.restartValues());
} catch (e) {
this.vnApp.showError(this.$t(e.message));
this.isInvoicing = false;
this.restartValues();
return false;
}
}

View File

@ -19,6 +19,7 @@ describe('InvoiceOut', () => {
}
};
controller = $componentController('vnInvoiceOutGlobalInvoicing', {$element, $scope, $transclude});
controller.$.invoiceButton = {disabled: false};
}));
describe('getMinClientId()', () => {
@ -87,14 +88,26 @@ describe('InvoiceOut', () => {
it('should make an http POST query and then call to the showSuccess() method', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
const minShipped = new Date();
minShipped.setFullYear(minShipped.getFullYear() - 1);
minShipped.setMonth(1);
minShipped.setDate(1);
minShipped.setHours(0, 0, 0, 0);
controller.invoice = {
invoiceDate: new Date(),
maxShipped: new Date(),
fromClientId: 1101,
toClientId: 1101
toClientId: 1101,
companyFk: 442,
minShipped: minShipped
};
const response = {
clientsAndAddresses: [{clientId: 1101, addressId: 121}],
invoice: controller.invoice
};
$httpBackend.expect('POST', `InvoiceOuts/globalInvoicing`).respond({id: 1});
$httpBackend.expect('POST', `InvoiceOuts/clientsToInvoice`).respond(response);
$httpBackend.expect('POST', `InvoiceOuts/invoiceClient`).respond({id: 1});
controller.responseHandler('accept');
$httpBackend.flush();

View File

@ -7,3 +7,8 @@ From client: Desde el cliente
To client: Hasta el cliente
Invoice date and the max date should be filled: La fecha de factura y la fecha límite deben rellenarse
Choose a valid clients range: Selecciona un rango válido de clientes
of: de
Id Client: Id Cliente
All clients: Todos los clientes
Clients range: Rango de clientes
Calculating packages to invoice...: Calculando paquetes a factura...

View File

@ -30,7 +30,6 @@
<vn-th field="companyFk">Company</vn-th>
<vn-th field="dued" expand>Due date</vn-th>
<vn-th></vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>

View File

@ -85,7 +85,7 @@
</span>
</vn-td>
<vn-td expand>{{ticket.shipped | date: 'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
<vn-td number>{{ticket.totalWithVat | currency: 'EUR': 2}}</vn-td>
<vn-td number expand>{{ticket.totalWithVat | currency: 'EUR': 2}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>

View File

@ -49,6 +49,10 @@ module.exports = Self => {
const provisionalName = params.provisionalName;
delete params.provisionalName;
const itemType = await models.ItemType.findById(params.typeFk, myOptions);
params.isLaid = itemType.isLaid;
const item = await models.Item.create(params, myOptions);
const typeTags = await models.ItemTypeTag.find({

View File

@ -15,6 +15,11 @@ describe('item new()', () => {
};
let item = await models.Item.new(itemParams, options);
let itemType = await models.ItemType.findById(item.typeFk, options);
item.isLaid = itemType.isLaid;
const temporalNameTag = await models.Tag.findOne({where: {name: 'Nombre temporal'}}, options);
const temporalName = await models.ItemTag.findOne({
@ -26,9 +31,14 @@ describe('item new()', () => {
item = await models.Item.findById(item.id, null, options);
itemType = await models.ItemType.findById(item.typeFk, options);
item.isLaid = itemType.isLaid;
expect(item.intrastatFk).toEqual(5080000);
expect(item.originFk).toEqual(1);
expect(item.typeFk).toEqual(2);
expect(item.isLaid).toBeFalse();
expect(item.name).toEqual('planta');
expect(temporalName.value).toEqual('planta');

View File

@ -26,6 +26,9 @@
},
"isUnconventionalSize": {
"type": "number"
},
"isLaid": {
"type": "boolean"
}
},
"relations": {

View File

@ -143,6 +143,9 @@
},
"packingShelve": {
"type": "number"
},
"isLaid": {
"type": "boolean"
}
},
"relations": {

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethodCtx('sendSms', {
description: 'Sends a SMS to each client of the routes, each client only recieves the SMS once',
accessType: 'WRITE',
accepts: [
{
arg: 'destination',
type: 'string',
description: 'A comma separated string of destinations',
required: true,
},
{
arg: 'message',
type: 'string',
required: true,
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/sendSms`,
verb: 'POST'
}
});
Self.sendSms = async(ctx, destination, message) => {
const targetClients = destination.split(',');
const allSms = [];
for (let client of targetClients) {
let sms = await Self.app.models.Sms.send(ctx, client, message);
allSms.push(sms);
}
return allSms;
};
};

View File

@ -12,6 +12,7 @@ module.exports = Self => {
require('../methods/route/updateWorkCenter')(Self);
require('../methods/route/driverRoutePdf')(Self);
require('../methods/route/driverRouteEmail')(Self);
require('../methods/route/sendSms')(Self);
Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000'

View File

@ -15,3 +15,4 @@ import './agency-term/index';
import './agency-term/createInvoiceIn';
import './agency-term-search-panel';
import './ticket-popup';
import './sms';

View File

@ -9,3 +9,5 @@ Go to route: Ir a la ruta
You must select a valid time: Debe seleccionar una hora válida
You must select a valid date: Debe seleccionar una fecha válida
Mark as served: Marcar como servidas
Retrieving data from the routes: Recuperando datos de las rutas
Send SMS to all clients: Mandar sms a todos los clientes de las rutas

View File

@ -0,0 +1,36 @@
<vn-dialog
vn-id="SMSDialog"
on-accept="$ctrl.onResponse()"
message="Send SMS to the selected tickets">
<tpl-body>
<section class="SMSDialog">
<vn-horizontal>
<vn-textarea vn-one
vn-id="message"
label="Message"
ng-model="$ctrl.sms.message"
info="Special characters like accents counts as a multiple"
rows="5"
required="true"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<span>
{{'Characters remaining' | translate}}:
<vn-chip translate-attr="{title: 'Packing'}" ng-class="{
'colored': $ctrl.charactersRemaining() > 25,
'warning': $ctrl.charactersRemaining() <= 25,
'alert': $ctrl.charactersRemaining() < 0,
}">
{{$ctrl.charactersRemaining()}}
</vn-chip>
</span>
</vn-horizontal>
</section>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Send</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,47 @@
import ngModule from '../module';
import Component from 'core/lib/component';
import './style.scss';
class Controller extends Component {
open() {
this.$.SMSDialog.show();
}
charactersRemaining() {
const element = this.$.message;
const value = element.input.value;
const maxLength = 160;
const textAreaLength = new Blob([value]).size;
return maxLength - textAreaLength;
}
onResponse() {
try {
if (!this.sms.destination)
throw new Error(`The destination can't be empty`);
if (!this.sms.message)
throw new Error(`The message can't be empty`);
if (this.charactersRemaining() < 0)
throw new Error(`The message it's too long`);
this.$http.post(`Routes/sendSms`, this.sms).then(res => {
this.vnApp.showMessage(this.$t('SMS sent!'));
if (res.data) this.emit('send', {response: res.data});
});
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
}
}
ngModule.vnComponent('vnRouteSms', {
template: require('./index.html'),
controller: Controller,
bindings: {
sms: '<',
}
});

View File

@ -0,0 +1,71 @@
import './index';
describe('Route', () => {
describe('Component vnRouteSms', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('route'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
let $scope = $rootScope.$new();
const $element = angular.element('<vn-dialog></vn-dialog>');
controller = $componentController('vnRouteSms', {$element, $scope});
controller.$.message = {
input: {
value: 'My SMS'
}
};
}));
describe('onResponse()', () => {
it('should perform a POST query and show a success snackbar', () => {
let params = {destinationFk: 1101, destination: 111111111, message: 'My SMS'};
controller.sms = {destinationFk: 1101, destination: 111111111, message: 'My SMS'};
jest.spyOn(controller.vnApp, 'showMessage');
$httpBackend.expect('POST', `Routes/sendSms`, params).respond(200, params);
controller.onResponse();
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('SMS sent!');
});
it('should call onResponse without the destination and show an error snackbar', () => {
controller.sms = {destinationFk: 1101, message: 'My SMS'};
jest.spyOn(controller.vnApp, 'showError');
controller.onResponse();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`The destination can't be empty`);
});
it('should call onResponse without the message and show an error snackbar', () => {
controller.sms = {destinationFk: 1101, destination: 222222222};
jest.spyOn(controller.vnApp, 'showError');
controller.onResponse();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`The message can't be empty`);
});
});
describe('charactersRemaining()', () => {
it('should return the characters remaining in a element', () => {
controller.$.message = {
input: {
value: 'My message 0€'
}
};
let result = controller.charactersRemaining();
expect(result).toEqual(145);
});
});
});
});

View File

@ -0,0 +1,9 @@
Send SMS to the selected tickets: Enviar SMS a los tickets seleccionados
Routes to notify: Rutas a notificar
Message: Mensaje
SMS sent!: ¡SMS enviado!
Characters remaining: Carácteres restantes
The destination can't be empty: El destinatario no puede estar vacio
The message can't be empty: El mensaje no puede estar vacio
The message it's too long: El mensaje es demasiado largo
Special characters like accents counts as a multiple: Carácteres especiales como los acentos cuentan como varios

View File

@ -0,0 +1,5 @@
@import "variables";
.SMSDialog {
min-width: 400px
}

View File

@ -29,13 +29,21 @@
disabled="!$ctrl.isChecked"
ng-click="$ctrl.deletePriority()"
vn-tooltip="Delete priority"
icon="filter_alt_off">
icon="filter_alt">
</vn-button>
<vn-button
ng-click="$ctrl.setOrderedPriority($ctrl.tickets)"
vn-tooltip="Renumber all tickets in the order you see on the screen"
icon="format_list_numbered">
</vn-button>
<vn-button
vn-acl="deliveryBoss"
vn-acl-action="remove"
disabled="!$ctrl.isChecked"
icon="sms"
vn-tooltip="Send SMS to all clients"
ng-click="$ctrl.sendSms()">
</vn-button>
</vn-tool-bar>
<vn-table class="vn-pt-md" model="model" auto-load="false" vn-droppable="$ctrl.onDrop($event)">
<vn-thead>
@ -149,19 +157,29 @@
route="$ctrl.$params"
parent-reload="$ctrl.$.model.refresh()">
</vn-route-ticket-popup>
<vn-float-button
icon="add"
<div fixed-bottom-right>
<vn-vertical style="align-items: center;">
<a vn-bind="+">
<vn-button
class="round md vn-mb-sm"
ng-click="$ctrl.$.ticketPopup.show()"
icon="add"
vn-tooltip="Add ticket"
vn-acl="delivery"
vn-acl-action="remove"
vn-bind="+"
fixed-bottom-right>
</vn-float-button>
tooltip-position="left">
</vn-button>
</a>
</vn-vertical>
</div>
<vn-ticket-descriptor-popover
vn-id="ticket-descriptor">
</vn-ticket-descriptor-popover>
<vn-client-descriptor-popover
vn-id="client-descriptor">
</vn-client-descriptor-popover>
<!-- SMS Dialog -->
<vn-route-sms
vn-id="sms"
sms="$ctrl.newSMS">
</vn-route-sms>

View File

@ -161,6 +161,37 @@ class Controller extends Section {
throw error;
});
}
async sendSms() {
try {
const clientsFk = [];
const clientsName = [];
const clients = [];
const selectedTickets = this.getSelectedItems(this.$.$ctrl.tickets);
for (let ticket of selectedTickets) {
clientsFk.push(ticket.clientFk);
let userContact = await this.$http.get(`Clients/${ticket.clientFk}`);
clientsName.push(userContact.data.name);
clients.push(userContact.data.phone);
}
const destinationFk = String(clientsFk);
const destination = String(clients);
this.newSMS = Object.assign({
destinationFk: destinationFk,
destination: destination
});
this.$.sms.open();
return true;
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
}
}
ngModule.vnComponent('vnRouteTickets', {

View File

@ -1,10 +1,12 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('canEdit', {
description: 'Check if all the received sales are aditable',
accessType: 'READ',
accepts: [{
arg: 'sales',
type: ['object'],
type: ['number'],
required: true
}],
returns: {
@ -12,29 +14,48 @@ module.exports = Self => {
root: true
},
http: {
path: `/isEditable`,
verb: 'get'
path: `/canEdit`,
verb: 'GET'
}
});
Self.canEdit = async(ctx, sales, options) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const idsCollection = sales.map(sale => sale.id);
const salesData = await models.Sale.find({
fields: ['id', 'itemFk', 'ticketFk'],
where: {id: {inq: sales}},
include:
{
relation: 'item',
scope: {
fields: ['id', 'isFloramondo'],
}
}
}, myOptions);
const saleTracking = await models.SaleTracking.find({where: {saleFk: {inq: idsCollection}}}, myOptions);
const ticketId = salesData[0].ticketFk;
const hasSaleTracking = saleTracking.length;
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId, myOptions);
if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const isProductionRole = await models.Account.hasRole(userId, 'production', myOptions);
const hasSaleTracking = await models.SaleTracking.findOne({where: {saleFk: {inq: sales}}}, myOptions);
const hasSaleCloned = await models.SaleCloned.findOne({where: {saleClonedFk: {inq: sales}}}, myOptions);
const hasSaleFloramondo = salesData.find(sale => sale.item().isFloramondo);
const canEdit = (isProductionRole || !hasSaleTracking);
const canEditTracked = await models.ACL.checkAccessAcl(ctx, 'Sale', 'editTracked');
const canEditCloned = await models.ACL.checkAccessAcl(ctx, 'Sale', 'editCloned');
const canEditFloramondo = await models.ACL.checkAccessAcl(ctx, 'Sale', 'editFloramondo');
return canEdit;
const shouldEditTracked = canEditTracked || !hasSaleTracking;
const shouldEditCloned = canEditCloned || !hasSaleCloned;
const shouldEditFloramondo = canEditFloramondo || !hasSaleFloramondo;
return shouldEditTracked && shouldEditCloned && shouldEditFloramondo;
};
};

View File

@ -41,7 +41,11 @@ module.exports = Self => {
}
try {
const canEditSales = await models.Sale.canEdit(ctx, sales, myOptions);
const saleIds = sales.map(sale => sale.id);
const canEditSales = await models.Sale.canEdit(ctx, saleIds, myOptions);
if (!canEditSales)
throw new UserError(`Sale(s) blocked, please contact production`);
const ticket = await models.Ticket.findById(ticketId, {
include: {
@ -57,13 +61,6 @@ module.exports = Self => {
}
}, myOptions);
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId, myOptions);
if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`);
if (!canEditSales)
throw new UserError(`Sale(s) blocked, please contact production`);
const promises = [];
let deletions = '';
for (let sale of sales) {

View File

@ -1,4 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('recalculatePrice', {
description: 'Calculates the price of sales and its components',
@ -34,15 +35,9 @@ module.exports = Self => {
}
try {
const salesIds = [];
for (let sale of sales)
salesIds.push(sale.id);
const salesIds = sales.map(sale => sale.id);
const isEditable = await models.Ticket.isEditable(ctx, sales[0].ticketFk, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const canEditSale = await models.Sale.canEdit(ctx, sales, myOptions);
const canEditSale = await models.Sale.canEdit(ctx, salesIds, myOptions);
if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`);

View File

@ -49,12 +49,9 @@ module.exports = Self => {
}
try {
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId, myOptions);
if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const canEditSale = await models.Sale.canEdit(ctx, sales, myOptions);
const salesIds = sales.map(sale => sale.id);
const canEditSale = await models.Sale.canEdit(ctx, salesIds, myOptions);
if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`);

View File

@ -1,6 +1,9 @@
const models = require('vn-loopback/server/server').models;
describe('sale canEdit()', () => {
const employeeId = 1;
describe('sale editTracked', () => {
it('should return true if the role is production regardless of the saleTrackings', async() => {
const tx = await models.Sale.beginTransaction({});
@ -10,7 +13,7 @@ describe('sale canEdit()', () => {
const productionUserID = 49;
const ctx = {req: {accessToken: {userId: productionUserID}}};
const sales = [{id: 3}];
const sales = [25];
const result = await models.Sale.canEdit(ctx, sales, options);
@ -32,7 +35,7 @@ describe('sale canEdit()', () => {
const salesPersonUserID = 18;
const ctx = {req: {accessToken: {userId: salesPersonUserID}}};
const sales = [{id: 10}];
const sales = [10];
const result = await models.Sale.canEdit(ctx, sales, options);
@ -51,10 +54,10 @@ describe('sale canEdit()', () => {
try {
const options = {transaction: tx};
const salesPersonUserID = 18;
const ctx = {req: {accessToken: {userId: salesPersonUserID}}};
const buyerId = 35;
const ctx = {req: {accessToken: {userId: buyerId}}};
const sales = [{id: 3}];
const sales = [31];
const result = await models.Sale.canEdit(ctx, sales, options);
@ -66,4 +69,125 @@ describe('sale canEdit()', () => {
throw e;
}
});
});
describe('sale editCloned', () => {
const saleCloned = [29];
it('should return false if any of the sales is cloned', async() => {
const tx = await models.Sale.beginTransaction({});
try {
const options = {transaction: tx};
const buyerId = 35;
const ctx = {req: {accessToken: {userId: buyerId}}};
const result = await models.Sale.canEdit(ctx, saleCloned, options);
expect(result).toEqual(false);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return true if any of the sales is cloned and has the correct role', async() => {
const tx = await models.Sale.beginTransaction({});
const roleEnabled = await models.ACL.findOne({
where: {
model: 'Sale',
property: 'editCloned',
permission: 'ALLOW'
}
});
if (!roleEnabled || !roleEnabled.principalId) return await tx.rollback();
try {
const options = {transaction: tx};
const role = await models.Role.findOne({
where: {
name: roleEnabled.principalId
}
});
const ctx = {req: {accessToken: {userId: role.id}}};
const result = await models.Sale.canEdit(ctx, saleCloned, options);
expect(result).toEqual(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
describe('sale editFloramondo', () => {
it('should return false if any of the sales isFloramondo', async() => {
const tx = await models.Sale.beginTransaction({});
const sales = [26];
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: employeeId}}};
// For test
const saleToEdit = await models.Sale.findById(sales[0], null, options);
await saleToEdit.updateAttribute('itemFk', 9, options);
const result = await models.Sale.canEdit(ctx, sales, options);
expect(result).toEqual(false);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return true if any of the sales is of isFloramondo and has the correct role', async() => {
const tx = await models.Sale.beginTransaction({});
const sales = [26];
const roleEnabled = await models.ACL.findOne({
where: {
model: 'Sale',
property: 'editFloramondo',
permission: 'ALLOW'
}
});
if (!roleEnabled || !roleEnabled.principalId) return await tx.rollback();
try {
const options = {transaction: tx};
const role = await models.Role.findOne({
where: {
name: roleEnabled.principalId
}
});
const ctx = {req: {accessToken: {userId: role.id}}};
// For test
const saleToEdit = await models.Sale.findById(sales[0], null, options);
await saleToEdit.updateAttribute('itemFk', 9, options);
const result = await models.Sale.canEdit(ctx, sales, options);
expect(result).toEqual(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
});

View File

@ -3,7 +3,7 @@ const models = require('vn-loopback/server/server').models;
describe('sale reserve()', () => {
const ctx = {
req: {
accessToken: {userId: 9},
accessToken: {userId: 1},
headers: {origin: 'localhost:5000'},
__: () => {}
}

View File

@ -2,7 +2,7 @@ const models = require('vn-loopback/server/server').models;
describe('sale updateConcept()', () => {
const ctx = {req: {accessToken: {userId: 9}}};
const saleId = 1;
const saleId = 25;
it('should throw if ID was undefined', async() => {
const tx = await models.Sale.beginTransaction({});

View File

@ -9,32 +9,21 @@ describe('sale updateQuantity()', () => {
}
};
it('should throw an error if the quantity is not a number', async() => {
const tx = await models.Sale.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, 1, 'wrong quantity!', options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error('The value should be a number'));
});
it('should throw an error if the quantity is greater than it should be', async() => {
const ctx = {
req: {
accessToken: {userId: 1},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const tx = await models.Sale.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, 1, 99, options);
await models.Sale.updateQuantity(ctx, 17, 99, options);
await tx.rollback();
} catch (e) {
@ -45,21 +34,60 @@ describe('sale updateQuantity()', () => {
expect(error).toEqual(new Error('The new quantity should be smaller than the old one'));
});
it('should update the quantity of a given sale current line', async() => {
it('should add quantity if the quantity is greater than it should be and is role advanced', async() => {
const tx = await models.Sale.beginTransaction({});
const saleId = 17;
const buyerId = 35;
const ctx = {
req: {
accessToken: {userId: buyerId},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
try {
const options = {transaction: tx};
const originalLine = await models.Sale.findOne({where: {id: 1}, fields: ['quantity']}, options);
const isRoleAdvanced = await models.Ticket.isRoleAdvanced(ctx, options);
expect(originalLine.quantity).toEqual(5);
expect(isRoleAdvanced).toEqual(true);
await models.Sale.updateQuantity(ctx, 1, 4, options);
const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
const modifiedLine = await models.Sale.findOne({where: {id: 1}, fields: ['quantity']}, options);
expect(originalLine.quantity).toEqual(30);
expect(modifiedLine.quantity).toEqual(4);
const newQuantity = originalLine.quantity + 1;
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(modifiedLine.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should update the quantity of a given sale current line', async() => {
const tx = await models.Sale.beginTransaction({});
const saleId = 25;
const newQuantity = 4;
try {
const options = {transaction: tx};
const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(originalLine.quantity).toEqual(20);
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(modifiedLine.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {

View File

@ -66,12 +66,7 @@ module.exports = Self => {
const sale = await models.Sale.findById(id, filter, myOptions);
const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const canEditSale = await models.Sale.canEdit(ctx, [id], myOptions);
if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`);

View File

@ -42,13 +42,9 @@ module.exports = Self => {
try {
const canEditSale = await models.Sale.canEdit(ctx, [id], myOptions);
if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`);
if (isNaN(newQuantity))
throw new UserError(`The value should be a number`);
const filter = {
include: {
relation: 'ticket',
@ -70,7 +66,8 @@ module.exports = Self => {
const sale = await models.Sale.findById(id, filter, myOptions);
if (newQuantity > sale.quantity)
const isRoleAdvanced = await models.Ticket.isRoleAdvanced(ctx, myOptions);
if (newQuantity > sale.quantity && !isRoleAdvanced)
throw new UserError('The new quantity should be smaller than the old one');
const oldQuantity = sale.quantity;

View File

@ -17,7 +17,7 @@ describe('ticket-weekly filter()', () => {
const firstRow = result[0];
expect(firstRow.ticketFk).toEqual(1);
expect(result.length).toEqual(5);
expect(result.length).toEqual(6);
await tx.rollback();
} catch (e) {

View File

@ -20,24 +20,20 @@ module.exports = Self => {
});
Self.isEditable = async(ctx, id, options) => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
let state = await Self.app.models.TicketState.findOne({
const state = await models.TicketState.findOne({
where: {ticketFk: id}
}, myOptions);
const isSalesAssistant = await Self.app.models.Account.hasRole(userId, 'salesAssistant', myOptions);
const isDeliveryBoss = await Self.app.models.Account.hasRole(userId, 'deliveryBoss', myOptions);
const isBuyer = await Self.app.models.Account.hasRole(userId, 'buyer', myOptions);
const isRoleAdvanced = await models.Ticket.isRoleAdvanced(ctx, myOptions);
const isValidRole = isSalesAssistant || isDeliveryBoss || isBuyer;
let alertLevel = state ? state.alertLevel : null;
let ticket = await Self.app.models.Ticket.findById(id, {
const alertLevel = state ? state.alertLevel : null;
const ticket = await models.Ticket.findById(id, {
fields: ['clientFk'],
include: [{
relation: 'client',
@ -48,15 +44,17 @@ module.exports = Self => {
}
}]
}, myOptions);
const isLocked = await Self.app.models.Ticket.isLocked(id, myOptions);
const isLocked = await models.Ticket.isLocked(id, myOptions);
const isWeekly = await models.TicketWeekly.findOne({where: {ticketFk: id}}, myOptions);
const alertLevelGreaterThanZero = (alertLevel && alertLevel > 0);
const isNormalClient = ticket && ticket.client().type().code == 'normal';
const validAlertAndRoleNormalClient = (alertLevelGreaterThanZero && isNormalClient && !isValidRole);
if (!ticket || validAlertAndRoleNormalClient || isLocked)
return false;
const isEditable = !(alertLevelGreaterThanZero && isNormalClient);
if (ticket && (isEditable || isRoleAdvanced) && !isLocked && !isWeekly)
return true;
return false;
};
};

View File

@ -0,0 +1,32 @@
module.exports = Self => {
Self.remoteMethodCtx('isRoleAdvanced', {
description: 'Check if a ticket is editable',
accessType: 'READ',
returns: {
type: 'boolean',
root: true
},
http: {
path: `/isRoleAdvanced`,
verb: 'GET'
}
});
Self.isRoleAdvanced = async(ctx, options) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions);
const isDeliveryBoss = await models.Account.hasRole(userId, 'deliveryBoss', myOptions);
const isBuyer = await models.Account.hasRole(userId, 'buyer', myOptions);
const isClaimManager = await models.Account.hasRole(userId, 'claimManager', myOptions);
const isRoleAdvanced = isSalesAssistant || isDeliveryBoss || isBuyer || isClaimManager;
return isRoleAdvanced;
};
};

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('ticket componentUpdate()', () => {
const userID = 1101;
@ -178,10 +179,15 @@ describe('ticket componentUpdate()', () => {
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: ctx.req
});
const oldTicket = await models.Ticket.findById(ticketID, null, options);
await models.Ticket.componentUpdate(ctx, options);
const [newTicketID] = await models.Ticket.rawSql('SELECT MAX(id) as id FROM ticket', null, options);
const oldTicket = await models.Ticket.findById(ticketID, null, options);
const newTicket = await models.Ticket.findById(newTicketID.id, null, options);
const newTicketSale = await models.Sale.findOne({where: {ticketFk: args.id}}, options);

View File

@ -134,4 +134,23 @@ describe('ticket isEditable()', () => {
expect(result).toEqual(false);
});
it('should not be able to edit if is a ticket weekly', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}};
const result = await models.Ticket.isEditable(ctx, 15, options);
expect(result).toEqual(false);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -35,6 +35,9 @@
"SaleChecked": {
"dataSource": "vn"
},
"SaleCloned": {
"dataSource": "vn"
},
"SaleComponent": {
"dataSource": "vn"
},

View File

@ -0,0 +1,26 @@
{
"name": "SaleCloned",
"base": "VnModel",
"options": {
"mysql": {
"table": "saleCloned"
}
},
"properties": {
"saleClonedFk": {
"id": true
}
},
"relations": {
"saleOriginal": {
"type": "belongsTo",
"model": "Sale",
"foreignKey": "saleOriginalFk"
},
"saleCloned": {
"type": "belongsTo",
"model": "Sale",
"foreignKey": "saleClonedFk"
}
}
}

View File

@ -33,5 +33,6 @@ module.exports = function(Self) {
require('../methods/ticket/closeByTicket')(Self);
require('../methods/ticket/closeByAgency')(Self);
require('../methods/ticket/closeByRoute')(Self);
require('../methods/ticket/isRoleAdvanced')(Self);
require('../methods/ticket/collectionLabel')(Self);
};

View File

@ -76,7 +76,7 @@ class Controller extends Component {
haveNotNegatives = true;
});
this.ticket.withoutNegatives = false;
this.ticket.withoutNegatives = true;
this.haveNegatives = (haveNegatives && haveNotNegatives && haveDifferences);
}

View File

@ -76,9 +76,10 @@ describe('Ticket', () => {
it('should make a query and then call to the $state go() method', () => {
jest.spyOn(controller.$state, 'go').mockReturnThis();
const landed = new Date();
const ticket = {
clientFk: 1101,
landed: new Date(),
landed: landed,
addressFk: 121,
agencyModeFk: 1,
warehouseFk: 1
@ -90,7 +91,7 @@ describe('Ticket', () => {
const expectedParams = {
clientId: 1101,
landed: new Date(),
landed: landed,
warehouseId: 1,
addressId: 121,
agencyModeId: 1,

View File

@ -1,181 +0,0 @@
const Imap = require('imap');
module.exports = Self => {
Self.remoteMethod('checkInbox', {
description: 'Check an email inbox and process it',
accessType: 'READ',
returns:
{
arg: 'body',
type: 'file',
root: true
},
http: {
path: `/checkInbox`,
verb: 'POST'
}
});
Self.checkInbox = async() => {
let imapConfig = await Self.app.models.WorkerTimeControlParams.findOne();
let imap = new Imap({
user: imapConfig.mailUser,
password: imapConfig.mailPass,
host: imapConfig.mailHost,
port: 993,
tls: true
});
let isEmailOk;
let uid;
let emailBody;
function openInbox(cb) {
imap.openBox('INBOX', true, cb);
}
imap.once('ready', function() {
openInbox(function(err, box) {
if (err) throw err;
const totalMessages = box.messages.total;
if (totalMessages == 0)
imap.end();
let f = imap.seq.fetch('1:*', {
bodies: ['HEADER.FIELDS (FROM SUBJECT)', '1'],
struct: true
});
f.on('message', function(msg, seqno) {
isEmailOk = false;
msg.on('body', function(stream, info) {
let buffer = '';
let bufferCopy = '';
stream.on('data', function(chunk) {
buffer = chunk.toString('utf8');
if (info.which === '1' && bufferCopy.length == 0)
bufferCopy = buffer.replace(/\s/g, ' ');
});
stream.on('end', function() {
if (bufferCopy.length > 0) {
emailBody = bufferCopy.toUpperCase().trim();
const bodyPositionOK = emailBody.match(/\bOK\b/i);
if (bodyPositionOK != null && (bodyPositionOK.index == 0 || bodyPositionOK.index == 122))
isEmailOk = true;
else
isEmailOk = false;
}
});
msg.once('attributes', function(attrs) {
uid = attrs.uid;
});
msg.once('end', function() {
if (info.which === 'HEADER.FIELDS (FROM SUBJECT)') {
if (isEmailOk) {
imap.move(uid, 'exito', function(err) {
});
emailConfirm(buffer);
} else {
imap.move(uid, 'error', function(err) {
});
emailReply(buffer, emailBody);
}
}
});
});
});
f.once('end', function() {
imap.end();
});
});
});
imap.connect();
return 'Leer emails de gestion horaria';
};
async function emailConfirm(buffer) {
const now = new Date();
const from = JSON.stringify(Imap.parseHeader(buffer).from);
const subject = JSON.stringify(Imap.parseHeader(buffer).subject);
const timeControlDate = await getEmailDate(subject);
const week = timeControlDate[0];
const year = timeControlDate[1];
const user = await getUser(from);
let workerMail;
if (user.id != null) {
workerMail = await Self.app.models.WorkerTimeControlMail.findOne({
where: {
week: week,
year: year,
workerFk: user.id
}
});
}
if (workerMail != null) {
await workerMail.updateAttributes({
updated: now,
state: 'CONFIRMED'
});
}
}
async function emailReply(buffer, emailBody) {
const now = new Date();
const from = JSON.stringify(Imap.parseHeader(buffer).from);
const subject = JSON.stringify(Imap.parseHeader(buffer).subject);
const timeControlDate = await getEmailDate(subject);
const week = timeControlDate[0];
const year = timeControlDate[1];
const user = await getUser(from);
let workerMail;
if (user.id != null) {
workerMail = await Self.app.models.WorkerTimeControlMail.findOne({
where: {
week: week,
year: year,
workerFk: user.id
}
});
if (workerMail != null) {
await workerMail.updateAttributes({
updated: now,
state: 'REVISE',
emailResponse: emailBody
});
} else
await sendMail(user, subject, emailBody);
}
}
async function getUser(workerEmail) {
const userEmail = workerEmail.match(/(?<=<)(.*?)(?=>)/);
let [user] = await Self.rawSql(`SELECT u.id,u.name FROM account.user u
LEFT JOIN account.mailForward m on m.account = u.id
WHERE forwardTo =? OR
CONCAT(u.name,'@verdnatura.es') = ?`,
[userEmail[0], userEmail[0]]);
return user;
}
async function getEmailDate(subject) {
const date = subject.match(/\d+/g);
return date;
}
async function sendMail(user, subject, emailBody) {
const sendTo = 'rrhh@verdnatura.es';
const emailSubject = subject + ' ' + user.name;
await Self.app.models.Mail.create({
receiver: sendTo,
subject: emailSubject,
body: emailBody
});
}
};

View File

@ -0,0 +1,377 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethodCtx('sendMail', {
description: `Send an email with the hours booked to the employees who telecommuting.
It also inserts booked hours in cases where the employee is telecommuting`,
accessType: 'WRITE',
accepts: [{
arg: 'workerId',
type: 'number',
description: 'The worker id'
},
{
arg: 'week',
type: 'number'
},
{
arg: 'year',
type: 'number'
}],
returns: [{
type: 'Object',
root: true
}],
http: {
path: `/sendMail`,
verb: 'POST'
}
});
Self.sendMail = async(ctx, options) => {
const models = Self.app.models;
const conn = Self.dataSource.connector;
const args = ctx.args;
const $t = ctx.req.__; // $translate
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
const stmts = [];
let stmt;
try {
if (!args.week || !args.year) {
const from = new Date();
const to = new Date();
const time = await models.Time.findOne({
where: {
dated: {between: [from.setDate(from.getDate() - 10), to.setDate(to.getDate() - 4)]}
},
order: 'week ASC'
}, myOptions);
args.week = time.week;
args.year = time.year;
}
const started = getStartDateOfWeekNumber(args.week, args.year);
started.setHours(0, 0, 0, 0);
const ended = new Date(started);
ended.setDate(started.getDate() + 6);
ended.setHours(23, 59, 59, 999);
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate');
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeBusinessCalculate');
if (args.workerId) {
await models.WorkerTimeControl.destroyAll({
userFk: args.workerId,
timed: {between: [started, ended]},
isSendMail: true
}, myOptions);
const where = {
workerFk: args.workerId,
year: args.year,
week: args.week
};
await models.WorkerTimeControlMail.updateAll(where, {
updated: new Date(), state: 'SENDED'
}, myOptions);
stmt = new ParameterizedSQL(
`CALL vn.timeControl_calculateByUser(?, ?, ?)
`, [args.workerId, started, ended]);
stmts.push(stmt);
stmt = new ParameterizedSQL(
`CALL vn.timeBusiness_calculateByUser(?, ?, ?)
`, [args.workerId, started, ended]);
stmts.push(stmt);
} else {
await models.WorkerTimeControl.destroyAll({
timed: {between: [started, ended]},
isSendMail: true
}, myOptions);
const where = {
year: args.year,
week: args.week
};
await models.WorkerTimeControlMail.updateAll(where, {
updated: new Date(), state: 'SENDED'
}, myOptions);
stmt = new ParameterizedSQL(`CALL vn.timeControl_calculateAll(?, ?)`, [started, ended]);
stmts.push(stmt);
stmt = new ParameterizedSQL(`CALL vn.timeBusiness_calculateAll(?, ?)`, [started, ended]);
stmts.push(stmt);
}
stmt = new ParameterizedSQL(`
SELECT CONCAT(u.name, '@verdnatura.es') receiver,
u.id workerFk,
tb.dated,
tb.timeWorkDecimal,
tb.timeWorkSexagesimal timeWorkSexagesimal,
tb.timeTable,
tc.timeWorkDecimal timeWorkedDecimal,
tc.timeWorkSexagesimal timeWorkedSexagesimal,
tb.type,
tb.businessFk,
tb.permissionRate,
d.isTeleworking
FROM tmp.timeBusinessCalculate tb
JOIN user u ON u.id = tb.userFk
JOIN department d ON d.id = tb.departmentFk
JOIN business b ON b.id = tb.businessFk
LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk AND tc.dated = tb.dated
LEFT JOIN worker w ON w.id = u.id
JOIN (SELECT tb.userFk,
SUM(IF(tb.type IS NULL,
IF(tc.timeWorkDecimal > 0, FALSE, IF(tb.timeWorkDecimal > 0, TRUE, FALSE)),
TRUE))isTeleworkingWeek
FROM tmp.timeBusinessCalculate tb
LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk
AND tc.dated = tb.dated
GROUP BY tb.userFk
HAVING isTeleworkingWeek > 0
)sub ON sub.userFk = u.id
WHERE d.hasToRefill
AND IFNULL(?, u.id) = u.id
AND b.companyCodeFk = 'VNL'
AND w.businessFk
ORDER BY u.id, tb.dated
`, [args.workerId]);
const index = stmts.push(stmt) - 1;
const sql = ParameterizedSQL.join(stmts, ';');
const days = await conn.executeStmt(sql, myOptions);
let previousWorkerFk = days[index][0].workerFk;
let previousReceiver = days[index][0].receiver;
const workerTimeControlConfig = await models.WorkerTimeControlConfig.findOne(null, myOptions);
for (let day of days[index]) {
workerFk = day.workerFk;
if (day.timeWorkDecimal > 0 && day.timeWorkedDecimal == null
&& (day.permissionRate ? day.permissionRate : true)) {
if (day.timeTable == null) {
const timed = new Date(day.dated);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(8),
manual: true,
direction: 'in',
isSendMail: true
}, myOptions);
if (day.timeWorkDecimal >= workerTimeControlConfig.timeToBreakTime / 3600) {
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(9),
manual: true,
direction: 'middle',
isSendMail: true
}, myOptions);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(9, 20),
manual: true,
direction: 'middle',
isSendMail: true
}, myOptions);
}
const [hoursWork, minutesWork, secondsWork] = getTime(day.timeWorkSexagesimal);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(8 + hoursWork, minutesWork, secondsWork),
manual: true,
direction: 'out',
isSendMail: true
}, myOptions);
} else {
const weekDay = day.dated.getDay();
const journeys = await models.Journey.find({
where: {
business_id: day.businessFk,
day_id: weekDay
}
}, myOptions);
let timeTableDecimalInSeconds = 0;
for (let journey of journeys) {
const start = new Date();
const [startHours, startMinutes, startSeconds] = getTime(journey.start);
start.setHours(startHours, startMinutes, startSeconds, 0);
const end = new Date();
const [endHours, endMinutes, endSeconds] = getTime(journey.end);
end.setHours(endHours, endMinutes, endSeconds, 0);
const result = (end - start) / 1000;
timeTableDecimalInSeconds += result;
}
for (let journey of journeys) {
const timeTableDecimal = timeTableDecimalInSeconds / 3600;
if (day.timeWorkDecimal == timeTableDecimal) {
const timed = new Date(day.dated);
const [startHours, startMinutes, startSeconds] = getTime(journey.start);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(startHours, startMinutes, startSeconds),
manual: true,
isSendMail: true
}, myOptions);
const [endHours, endMinutes, endSeconds] = getTime(journey.end);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(endHours, endMinutes, endSeconds),
manual: true,
isSendMail: true
}, myOptions);
} else {
const minStart = journeys.reduce(function(prev, curr) {
return curr.start < prev.start ? curr : prev;
});
if (journey == minStart) {
const timed = new Date(day.dated);
const [startHours, startMinutes, startSeconds] = getTime(journey.start);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(startHours, startMinutes, startSeconds),
manual: true,
isSendMail: true
}, myOptions);
const [hoursWork, minutesWork, secondsWork] = getTime(day.timeWorkSexagesimal);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(
startHours + hoursWork,
startMinutes + minutesWork,
startSeconds + secondsWork
),
manual: true,
isSendMail: true
}, myOptions);
}
}
if (day.timeWorkDecimal >= workerTimeControlConfig.timeToBreakTime / 3600) {
const minStart = journeys.reduce(function(prev, curr) {
return curr.start < prev.start ? curr : prev;
});
if (journey == minStart) {
const timed = new Date(day.dated);
const [startHours, startMinutes, startSeconds] = getTime(journey.start);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(startHours + 1, startMinutes, startSeconds),
manual: true,
isSendMail: true
}, myOptions);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(startHours + 1, startMinutes + 20, startSeconds),
manual: true,
isSendMail: true
}, myOptions);
}
}
}
const timed = new Date(day.dated);
const firstWorkerTimeControl = await models.WorkerTimeControl.findOne({
where: {
userFk: day.workerFk,
timed: {between: [timed.setHours(0, 0, 0, 0), timed.setHours(23, 59, 59, 999)]}
},
order: 'timed ASC'
}, myOptions);
if (firstWorkerTimeControl)
firstWorkerTimeControl.updateAttribute('direction', 'in', myOptions);
const lastWorkerTimeControl = await models.WorkerTimeControl.findOne({
where: {
userFk: day.workerFk,
timed: {between: [timed.setHours(0, 0, 0, 0), timed.setHours(23, 59, 59, 999)]}
},
order: 'timed DESC'
}, myOptions);
if (lastWorkerTimeControl)
lastWorkerTimeControl.updateAttribute('direction', 'out', myOptions);
}
}
const lastDay = days[index][days[index].length - 1];
if (day.workerFk != previousWorkerFk || day == lastDay) {
const salix = await models.Url.findOne({
where: {
appName: 'salix',
environment: process.env.NODE_ENV || 'dev'
}
}, myOptions);
const timestamp = started.getTime() / 1000;
await models.Mail.create({
receiver: previousReceiver,
subject: $t('Record of hours week', {
week: args.week,
year: args.year
}),
body: `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}`
}, myOptions);
query = `INSERT IGNORE INTO workerTimeControlMail (workerFk, year, week)
VALUES (?, ?, ?);`;
await Self.rawSql(query, [previousWorkerFk, args.year, args.week], myOptions);
previousWorkerFk = day.workerFk;
previousReceiver = day.receiver;
}
}
if (tx) await tx.commit();
return true;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
function getStartDateOfWeekNumber(week, year) {
const simple = new Date(year, 0, 1 + (week - 1) * 7);
const dow = simple.getDay();
const weekStart = simple;
if (dow <= 4)
weekStart.setDate(simple.getDate() - simple.getDay() + 1);
else
weekStart.setDate(simple.getDate() + 8 - simple.getDay());
return weekStart;
}
function getTime(timeString) {
const [hours, minutes, seconds] = timeString.split(':');
return [parseInt(hours), parseInt(minutes), parseInt(seconds)];
}
};

View File

@ -0,0 +1,132 @@
const models = require('vn-loopback/server/server').models;
describe('workerTimeControl sendMail()', () => {
const workerId = 18;
const ctx = {
req: {
__: value => {
return value;
}
},
args: {}
};
beforeAll(function() {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
});
it('should fill time control of a worker without records in Journey and with rest', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
await models.WorkerTimeControl.sendMail(ctx, options);
const workerTimeControl = await models.WorkerTimeControl.find({
where: {userFk: workerId}
}, options);
expect(workerTimeControl[0].timed.getHours()).toEqual(8);
expect(workerTimeControl[1].timed.getHours()).toEqual(9);
expect(`${workerTimeControl[2].timed.getHours()}:${workerTimeControl[2].timed.getMinutes()}`).toEqual('9:20');
expect(workerTimeControl[3].timed.getHours()).toEqual(16);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should fill time control of a worker without records in Journey and without rest', async() => {
const workdayOf20Hours = 3;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
query = `UPDATE business b
SET b.calendarTypeFk = ?
WHERE b.workerFk = ?; `;
await models.WorkerTimeControl.rawSql(query, [workdayOf20Hours, workerId], options);
await models.WorkerTimeControl.sendMail(ctx, options);
const workerTimeControl = await models.WorkerTimeControl.find({
where: {userFk: workerId}
}, options);
expect(workerTimeControl[0].timed.getHours()).toEqual(8);
expect(workerTimeControl[1].timed.getHours()).toEqual(12);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should fill time control of a worker with records in Journey and with rest', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
query = `INSERT INTO postgresql.journey(journey_id, day_id, start, end, business_id)
VALUES
(1, 1, '09:00:00', '13:00:00', ?),
(2, 1, '14:00:00', '19:00:00', ?);`;
await models.WorkerTimeControl.rawSql(query, [workerId, workerId, workerId], options);
await models.WorkerTimeControl.sendMail(ctx, options);
const workerTimeControl = await models.WorkerTimeControl.find({
where: {userFk: workerId}
}, options);
expect(workerTimeControl[0].timed.getHours()).toEqual(9);
expect(workerTimeControl[2].timed.getHours()).toEqual(10);
expect(`${workerTimeControl[3].timed.getHours()}:${workerTimeControl[3].timed.getMinutes()}`).toEqual('10:20');
expect(workerTimeControl[1].timed.getHours()).toEqual(13);
expect(workerTimeControl[4].timed.getHours()).toEqual(14);
expect(workerTimeControl[5].timed.getHours()).toEqual(19);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should fill time control of a worker with records in Journey and without rest', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
query = `INSERT INTO postgresql.journey(journey_id, day_id, start, end, business_id)
VALUES
(1, 1, '12:30:00', '14:00:00', ?);`;
await models.WorkerTimeControl.rawSql(query, [workerId, workerId, workerId], options);
await models.WorkerTimeControl.sendMail(ctx, options);
const workerTimeControl = await models.WorkerTimeControl.find({
where: {userFk: workerId}
}, options);
expect(`${workerTimeControl[0].timed.getHours()}:${workerTimeControl[0].timed.getMinutes()}`).toEqual('12:30');
expect(workerTimeControl[1].timed.getHours()).toEqual(14);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
afterAll(function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
});

View File

@ -0,0 +1,87 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('updateWorkerTimeControlMail', {
description: 'Updates the state of WorkerTimeControlMail',
accessType: 'WRITE',
accepts: [{
arg: 'workerId',
type: 'number',
required: true
},
{
arg: 'year',
type: 'number',
required: true
},
{
arg: 'week',
type: 'number',
required: true
},
{
arg: 'state',
type: 'string',
required: true
},
{
arg: 'reason',
type: 'string'
}],
returns: {
type: 'boolean',
root: true
},
http: {
path: `/updateWorkerTimeControlMail`,
verb: 'POST'
}
});
Self.updateWorkerTimeControlMail = async(ctx, options) => {
const models = Self.app.models;
const args = ctx.args;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const workerTimeControlMail = await models.WorkerTimeControlMail.findOne({
where: {
workerFk: args.workerId,
year: args.year,
week: args.week
}
}, myOptions);
if (!workerTimeControlMail) throw new UserError(`There aren't records for this week`);
const oldState = workerTimeControlMail.state;
const oldReason = workerTimeControlMail.reason;
if (oldState == args.state) throw new UserError('Already has this status');
await workerTimeControlMail.updateAttributes({
state: args.state,
reason: args.reason || null
}, myOptions);
const logRecord = {
originFk: args.workerId,
userFk: userId,
action: 'update',
changedModel: 'WorkerTimeControlMail',
oldInstance: {
state: oldState,
reason: oldReason
},
newInstance: {
state: args.state,
reason: args.reason
}
};
return models.WorkerLog.create(logRecord, myOptions);
};
};

View File

@ -20,6 +20,12 @@
"EducationLevel": {
"dataSource": "vn"
},
"Journey": {
"dataSource": "vn"
},
"Time": {
"dataSource": "vn"
},
"WorkCenter": {
"dataSource": "vn"
},
@ -59,6 +65,9 @@
"WorkerLog": {
"dataSource": "vn"
},
"WorkerTimeControlConfig": {
"dataSource": "vn"
},
"WorkerTimeControlParams": {
"dataSource": "vn"
},

View File

@ -0,0 +1,27 @@
{
"name": "Journey",
"base": "VnModel",
"options": {
"mysql": {
"table": "postgresql.journey"
}
},
"properties": {
"journey_id": {
"id": true,
"type": "number"
},
"day_id": {
"type": "number"
},
"start": {
"type": "date"
},
"end": {
"type": "date"
},
"business_id": {
"type": "number"
}
}
}

View File

@ -0,0 +1,21 @@
{
"name": "Time",
"base": "VnModel",
"options": {
"mysql": {
"table": "time"
}
},
"properties": {
"dated": {
"id": true,
"type": "date"
},
"year": {
"type": "number"
},
"week": {
"type": "number"
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "WorkerTimeControlConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "workerTimeControlConfig"
}
},
"properties": {
"id": {
"id": true,
"type": "number"
},
"timeToBreakTime": {
"type": "number"
}
}
}

View File

@ -1,3 +0,0 @@
module.exports = Self => {
require('../methods/worker-time-control-mail/checkInbox')(Self);
};

View File

@ -9,8 +9,7 @@
"properties": {
"id": {
"id": true,
"type": "number",
"required": true
"type": "number"
},
"workerFk": {
"type": "number"
@ -27,7 +26,7 @@
"updated": {
"type": "date"
},
"emailResponse": {
"reason": {
"type": "string"
}
},

View File

@ -5,6 +5,8 @@ module.exports = Self => {
require('../methods/worker-time-control/addTimeEntry')(Self);
require('../methods/worker-time-control/deleteTimeEntry')(Self);
require('../methods/worker-time-control/updateTimeEntry')(Self);
require('../methods/worker-time-control/sendMail')(Self);
require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')

View File

@ -22,6 +22,9 @@
},
"direction": {
"type": "string"
},
"isSendMail": {
"type": "boolean"
}
},
"relations": {

View File

@ -77,6 +77,18 @@
</vn-tfoot>
</vn-table>
</vn-card>
<vn-button-bar class="vn-pa-xs vn-w-lg">
<vn-button
label="Satisfied"
ng-click="$ctrl.isSatisfied()">
</vn-button>
<vn-button
label="Not satisfied"
ng-click="reason.show()">
</vn-button>
</vn-button-bar>
<vn-side-menu side="right">
<div class="vn-pa-md">
<div class="totalBox" style="text-align: center;">
@ -149,3 +161,20 @@
</vn-icon-button>
</vn-horizontal>
</vn-popover>
<vn-dialog
vn-id="reason"
on-accept="$ctrl.isUnsatisfied()">
<tpl-body>
<vn-textarea
label="Reason"
ng-model="$ctrl.reason"
required="true"
rows="3">
</vn-textarea>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>

View File

@ -294,6 +294,42 @@ class Controller extends Section {
this.$.editEntry.show($event);
}
getWeekNumber(currentDate) {
const startDate = new Date(currentDate.getFullYear(), 0, 1);
let days = Math.floor((currentDate - startDate) /
(24 * 60 * 60 * 1000));
return Math.ceil(days / 7);
}
isSatisfied() {
const weekNumber = this.getWeekNumber(this.date);
const params = {
workerId: this.worker.id,
year: this.date.getFullYear(),
week: weekNumber,
state: 'CONFIRMED'
};
const query = `WorkerTimeControls/updateWorkerTimeControlMail`;
this.$http.post(query, params).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
isUnsatisfied() {
const weekNumber = this.getWeekNumber(this.date);
const params = {
workerId: this.worker.id,
year: this.date.getFullYear(),
week: weekNumber,
state: 'REVISE',
reason: this.reason
};
const query = `WorkerTimeControls/updateWorkerTimeControlMail`;
this.$http.post(query, params).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
save() {
try {
const entry = this.selectedRow;

View File

@ -11,3 +11,6 @@ Are you sure you want to delete this entry?: ¿Seguro que quieres eliminarla?
Finish at: Termina a las
Entry removed: Fichada borrada
The entry type can't be empty: El tipo de fichada no puede quedar vacía
Satisfied: Conforme
Not satisfied: No conforme
Reason: Motivo

40261
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@
"jsdom": "^16.7.0",
"jszip": "^3.10.0",
"ldapjs": "^2.2.0",
"loopback": "^3.26.0",
"loopback": "^3.28.0",
"loopback-boot": "3.3.1",
"loopback-component-explorer": "^6.5.0",
"loopback-component-storage": "3.6.1",
@ -42,7 +42,7 @@
"puppeteer": "^18.0.5",
"read-chunk": "^3.2.0",
"require-yaml": "0.0.1",
"sharp": "^0.31.0",
"sharp": "^0.31.2",
"smbhash": "0.0.1",
"strong-error-handler": "^2.3.2",
"uuid": "^3.3.3",
@ -80,7 +80,7 @@
"html-loader-jest": "^0.2.1",
"html-webpack-plugin": "^4.0.0-beta.11",
"identity-obj-proxy": "^3.0.0",
"jasmine": "^4.1.0",
"jasmine": "^4.5.0",
"jasmine-reporters": "^2.4.0",
"jasmine-spec-reporter": "^7.0.0",
"jest": "^26.0.1",

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width" />
<meta name="format-detection" content="telephone=no" />
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<slot name="header">
<div class="grid-row">
<div class="grid-block">
<email-header v-bind="$props"></email-header>
</div>
</div>
</slot>
<slot></slot>
<slot name="footer">
<div class="grid-row">
<div class="grid-block">
<email-footer v-bind="$props"></email-footer>
</div>
</div>
</slot>
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,12 @@
const Component = require(`vn-print/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
module.exports = {
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build()
},
name: 'email-body',
};

View File

@ -3,7 +3,7 @@
<div class="buttons">
<div class="columns">
<div class="size50">
<a href="https://www.verdnatura.es" target="_blank">
<a href="https://verdnatura.es" target="_blank">
<div class="btn">
<!-- <span class="icon vn-pa-sm"><img v-bind:src="getEmailSrc('action.png')"/></span> -->
<span class="text vn-pa-sm">{{ $t('buttons.webAcccess')}}</span>

View File

@ -2,7 +2,7 @@ buttons:
webAcccess: Visit our website
info: Help us to improve
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 C/ Fenollar, 2. 46680 ALGEMESI
· www.verdnatura.es · clientes@verdnatura.es
· verdnatura.es · clientes@verdnatura.es
disclaimer: '- NOTICE - This message is private and confidential, and should be used
exclusively by the person receiving it. If you have received this message by mistake,
please notify the sender and delete that message and any attached documents that it may contain.

View File

@ -2,7 +2,7 @@ buttons:
webAcccess: Visita nuestra Web
info: Ayúdanos a mejorar
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 C/ Fenollar, 2. 46680 ALGEMESI
· www.verdnatura.es · clientes@verdnatura.es
· verdnatura.es · clientes@verdnatura.es
disclaimer: '- AVISO - Este mensaje es privado y confidencial, y debe ser utilizado
exclusivamente por la persona destinataria del mismo. Si has recibido este mensaje
por error, te rogamos lo comuniques al remitente y borres dicho mensaje y cualquier

View File

@ -2,7 +2,7 @@ buttons:
webAcccess: Visitez notre site web
info: Aidez-nous à améliorer
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 C/ Fenollar, 2. 46680 ALGEMESI
· www.verdnatura.es · clientes@verdnatura.es
· verdnatura.es · clientes@verdnatura.es
disclaimer: "- AVIS - Ce message est privé et confidentiel et doit être utilisé
exclusivement par le destinataire. Si vous avez reçu ce message par erreur,
veuillez en informer l'expéditeur et supprimer ce message ainsi que tous les

View File

@ -2,7 +2,7 @@ buttons:
webAcccess: Visite o nosso site
info: Ajude-nos a melhorar
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 C/ Fenollar, 2. 46680 ALGEMESI
· www.verdnatura.es · clientes@verdnatura.es
· verdnatura.es · clientes@verdnatura.es
disclaimer: '- AVISO - Esta mensagem é privada e confidencial e deve ser usada exclusivamente
pela pessoa que a recebe. Se você recebeu esta mensagem por engano, notifique o remetente e
exclua essa mensagem e todos os documentos anexos que ela possa conter.

View File

@ -1,7 +1,7 @@
<header>
<div class="logo">
<a href="https://www.verdnatura.es" target="_blank">
<img v-bind:src="getEmailSrc('logo-black.png')" alt="VerdNatura"/>
<a href="https://verdnatura.es" target="_blank">
<img v-bind:src="getEmailSrc('logo-black.png')" alt="VerdNatura" />
</a>
</div>
<div class="topbar"></div>

View File

@ -0,0 +1,10 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/report.css`,
`${vnPrintPath}/common/css/misc.css`])
.mergeStyles();

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid">
<tbody>
<tr>
<td>
<slot name="header">
<report-header v-bind="$props"></report-header>
</slot>
<slot></slot>
<slot name="footer">
<report-footer id="pageFooter" v-bind="$props"></report-footer>
</slot>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,12 @@
const Component = require(`vn-print/core/component`);
const reportHeader = new Component('report-header');
const reportFooter = new Component('report-footer');
module.exports = {
name: 'report-body',
components: {
'report-header': reportHeader.build(),
'report-footer': reportFooter.build()
},
};

View File

@ -1,2 +1,2 @@
company:
contactData: www.verdnatura.es - clientes@verdnatura.es
contactData: verdnatura.es - clientes@verdnatura.es

View File

@ -1,2 +1,2 @@
company:
contactData: www.verdnatura.es - clientes@verdnatura.es
contactData: verdnatura.es - clientes@verdnatura.es

View File

@ -1,2 +1,2 @@
company:
contactData: www.verdnatura.es - clientes@verdnatura.es
contactData: verdnatura.es - clientes@verdnatura.es

View File

@ -1,2 +1,2 @@
company:
contactData: · www.verdnatura.es · clientes@verdnatura.es
contactData: · verdnatura.es · clientes@verdnatura.es

View File

@ -32,7 +32,7 @@ class Email extends Component {
const rendered = await this.render();
const attachments = [];
const getAttachments = async(componentPath, files) => {
for (file of files) {
for (const file of files) {
const fileCopy = Object.assign({}, file);
const fileName = fileCopy.filename;
@ -54,14 +54,21 @@ class Email extends Component {
}
};
async function getSubcomponentAttachments(instance) {
if (instance.components) {
const components = instance.components;
for (let componentName in components) {
const component = components[componentName];
const componentPath = `./components/${componentName}`;
await getAttachments(componentPath, component.attachments);
if (component.components)
await getSubcomponentAttachments(component)
}
}
}
await getSubcomponentAttachments(instance)
if (this.attachments)
await getAttachments(this.path, this.attachments);

Some files were not shown because too many files have changed in this diff Show More