Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2034-postcode_autocomplete

This commit is contained in:
Joan Sanchez 2020-03-03 13:59:33 +01:00
commit f16553ecba
58 changed files with 1165 additions and 233 deletions

View File

@ -10,8 +10,7 @@ module.exports = Self => {
type: 'Number',
description: 'The document id',
http: {source: 'path'}
},
{
}, {
arg: 'warehouseId',
type: 'Number',
description: 'The warehouse id'
@ -44,9 +43,9 @@ module.exports = Self => {
}
});
Self.updateFile = async(ctx, id, warehouseId, companyId,
dmsTypeId, reference, description, hasFileAttached, options) => {
Self.updateFile = async(ctx, id, options) => {
const models = Self.app.models;
const args = ctx.args;
let tx;
let myOptions = {};
@ -60,20 +59,20 @@ module.exports = Self => {
}
try {
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, dmsTypeId);
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, args.dmsTypeId);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
const dms = await Self.findById(id, null, myOptions);
await dms.updateAttributes({
dmsTypeFk: dmsTypeId,
companyFk: companyId,
warehouseFk: warehouseId,
reference: reference,
description: description
dmsTypeFk: args.dmsTypeId,
companyFk: args.companyId,
warehouseFk: args.warehouseId,
reference: args.reference,
description: args.description
}, myOptions);
if (hasFileAttached)
if (args.hasFileAttached)
await uploadNewFile(ctx, dms, myOptions);
if (tx) await tx.commit();

View File

@ -1903,11 +1903,11 @@ INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `c
VALUES
(1, 'Facturas Recibidas', 'recibidas', NULL, NULL, 'invoiceIn'),
(2, 'Doc oficial', 'oficial', NULL, NULL, 'officialDoc'),
(3, 'Laboral', 'laboral', NULL, NULL, 'hhrrData'),
(3, 'Laboral', 'laboral', 37, 37, 'hhrrData'),
(4, 'Albaranes recibidos', 'entradas', NULL, NULL, 'deliveryNote'),
(5, 'Otros', 'otros', 1, 1, 'miscellaneous'),
(6, 'Pruebas', 'pruebas', NULL, NULL, 'tests'),
(7, 'IAE Clientes', 'IAE_Clientes', NULL, NULL, 'economicActivitiesTax'),
(7, 'IAE Clientes', 'IAE_Clientes', 1, 1, 'economicActivitiesTax'),
(8, 'Fiscal', 'fiscal', NULL, NULL, 'fiscal'),
(9, 'Vehiculos', 'vehiculos', NULL, NULL, 'vehicles'),
(10, 'Plantillas', 'plantillas', NULL, NULL, 'templates'),

View File

@ -9,6 +9,7 @@ export default {
invoiceOutButton: '.modules-menu > li[ui-sref="invoiceOut.index"]',
claimsButton: '.modules-menu > li[ui-sref="claim.index"]',
returnToModuleIndexButton: 'a[ui-sref="order.index"]',
homeButton: 'vn-topbar > div.side.start > a',
userMenuButton: '#user',
userLocalWarehouse: '.user-popover vn-autocomplete[ng-model="$ctrl.localWarehouseFk"]',
userLocalBank: '.user-popover vn-autocomplete[ng-model="$ctrl.localBankFk"]',
@ -192,7 +193,7 @@ export default {
},
dms: {
deleteFileButton: 'vn-client-dms-index vn-tr:nth-child(1) vn-icon-button[icon="delete"]',
firstDocWorker: 'vn-client-dms-index vn-td:nth-child(8) > span',
firstDocWorker: 'vn-client-dms-index vn-td:nth-child(7) > span',
firstDocWorkerDescriptor: '.vn-popover.shown vn-worker-descriptor',
acceptDeleteButton: '.vn-confirm.shown button[response="accept"]'
},
@ -630,6 +631,16 @@ export default {
createButton: 'button[type=submit]',
cancelButton: 'vn-button[href="#!/client/index"]'
},
orderSummary: {
header: 'vn-order-summary h5',
id: 'vn-order-summary vn-one:nth-child(1) > vn-label-value:nth-child(1) span',
alias: 'vn-order-summary vn-one:nth-child(1) > vn-label-value:nth-child(2) span',
consignee: 'vn-order-summary vn-one:nth-child(2) > vn-label-value:nth-child(3) span',
subtotal: 'vn-order-summary vn-one.taxes > p:nth-child(1)',
vat: 'vn-order-summary vn-one.taxes > p:nth-child(2)',
total: 'vn-order-summary vn-one.taxes > p:nth-child(3)',
sale: 'vn-order-summary vn-tbody > vn-tr',
},
orderCatalog: {
plantRealmButton: 'vn-order-catalog > vn-side-menu vn-icon[icon="icon-plant"]',
type: 'vn-autocomplete[data="$ctrl.itemTypes"]',
@ -700,6 +711,17 @@ export default {
firstTicketDeleteButton: 'vn-route-tickets vn-tr:nth-child(1) vn-icon[icon="delete"]',
confirmButton: '.vn-confirm.shown button[response="accept"]'
},
workerSummary: {
header: 'vn-worker-summary h5',
id: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(2) > section > span',
email: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(3) > section > span',
department: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(4) > section > span',
userId: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(2) > section > span',
userName: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(3) > section > span',
role: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(4) > section > span',
extension: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(5) > section > span',
},
workerBasicData: {
name: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.firstName"]',
surname: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.lastName"]',
@ -783,10 +805,25 @@ export default {
ticketOne: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(1)',
ticketTwo: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(2)'
},
travelBasicDada: {
reference: 'vn-travel-basic-data vn-textfield[ng-model="$ctrl.travel.ref"]',
agency: 'vn-travel-basic-data vn-autocomplete[ng-model="$ctrl.travel.agencyModeFk"]',
shippingDate: 'vn-travel-basic-data vn-date-picker[ng-model="$ctrl.travel.shipped"]',
deliveryDate: 'vn-travel-basic-data vn-date-picker[ng-model="$ctrl.travel.landed"]',
outputWarehouse: 'vn-travel-basic-data vn-autocomplete[ng-model="$ctrl.travel.warehouseOutFk"]',
inputWarehouse: 'vn-travel-basic-data vn-autocomplete[ng-model="$ctrl.travel.warehouseInFk"]',
delivered: 'vn-travel-basic-data vn-check[ng-model="$ctrl.travel.isDelivered"]',
received: 'vn-travel-basic-data vn-check[ng-model="$ctrl.travel.isReceived"]',
save: 'vn-travel-basic-data vn-submit[label="Save"]',
undoChanges: 'vn-travel-basic-data vn-button[label="Undo changes"]'
},
travelLog: {
firstLogFirstTD: 'vn-travel-log vn-tbody > vn-tr > vn-td:nth-child(1) > div'
},
travelThermograph: {
add: 'vn-travel-thermograph-index vn-float-button[icon="add"]',
thermographID: 'vn-travel-thermograph-create vn-autocomplete[ng-model="$ctrl.dms.thermographId"]',
uploadIcon: 'vn-travel-thermograph-create vn-icon[icon="cloud_upload"]',
uploadIcon: 'vn-travel-thermograph-create vn-icon[icon="attach_file"]',
createdThermograph: 'vn-travel-thermograph-index vn-tbody > vn-tr',
upload: 'vn-travel-thermograph-create button[type=submit]'
},
@ -806,5 +843,10 @@ export default {
header: 'vn-entry-summary > vn-card > h5',
reference: 'vn-entry-summary vn-label-value[label="Reference"]',
confirmed: 'vn-entry-summary vn-check[label="Confirmed"]',
},
entryDescriptor: {
agency: 'vn-entry-descriptor div.body vn-label-value:nth-child(3) span',
travelsQuicklink: 'vn-entry-descriptor vn-quick-links > a:nth-child(1)',
entriesQuicklink: 'vn-entry-descriptor vn-quick-links > a:nth-child(2)'
}
};

View File

@ -16,6 +16,12 @@ describe('Client Add notes path', () => {
await browser.close();
});
it(`should reach the notes index`, async() => {
let url = await page.expectURL('/note');
expect(url).toBe(true);
});
it(`should click on the add note button`, async() => {
await page.waitToClick(selectors.clientNotes.addNoteFloatButton);
let url = await page.expectURL('/note/create');

View File

@ -0,0 +1,71 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Worker summary path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'worker');
await page.accessToSearchResult('agencyNick');
});
afterAll(async() => {
await browser.close();
});
it('should reach the employee summary section', async() => {
const url = await page.expectURL('#!/worker/3/summary');
expect(url).toBe(true);
});
it('should check the summary contains the name and userName on the header', async() => {
const result = await page.waitToGetProperty(selectors.workerSummary.header, 'innerText');
expect(result).toEqual('agency agency');
});
it('should check the summary contains the basic data id', async() => {
const result = await page.waitToGetProperty(selectors.workerSummary.id, 'innerText');
expect(result).toEqual('3');
});
it('should check the summary contains the basic data email', async() => {
const result = await page.waitToGetProperty(selectors.workerSummary.email, 'innerText');
expect(result).toEqual('agency@verdnatura.es');
});
it('should check the summary contains the basic data department', async() => {
const result = await page.waitToGetProperty(selectors.workerSummary.department, 'innerText');
expect(result).toEqual('CAMARA');
});
it('should check the summary contains the user data id', async() => {
const result = await page.waitToGetProperty(selectors.workerSummary.userId, 'innerText');
expect(result).toEqual('3');
});
it('should check the summary contains the user data name', async() => {
const result = await page.waitToGetProperty(selectors.workerSummary.userName, 'innerText');
expect(result).toEqual('agency');
});
it('should check the summary contains the user data role', async() => {
const result = await page.waitToGetProperty(selectors.workerSummary.role, 'innerText');
expect(result).toEqual('agency');
});
it('should check the summary contains the user data extension', async() => {
const result = await page.waitToGetProperty(selectors.workerSummary.extension, 'innerText');
expect(result).toEqual('1101');
});
});

View File

@ -0,0 +1,65 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order summary path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('16');
});
afterAll(async() => {
await browser.close();
});
it('should reach the order summary section', async() => {
const url = await page.expectURL('#!/order/16/summary');
expect(url).toBe(true);
});
it('should check the summary contains the order id', async() => {
const result = await page.waitToGetProperty(selectors.orderSummary.id, 'innerText');
expect(result).toEqual('16');
});
it('should check the summary contains the order alias', async() => {
const result = await page.waitToGetProperty(selectors.orderSummary.alias, 'innerText');
expect(result).toEqual('address 26');
});
it('should check the summary contains the order consignee', async() => {
const result = await page.waitToGetProperty(selectors.orderSummary.consignee, 'innerText');
expect(result).toEqual('Many places - Silla (Province one)');
});
it('should check the summary contains the order subtotal', async() => {
const result = await page.waitToGetProperty(selectors.orderSummary.subtotal, 'innerText');
expect(result.length).toBeGreaterThan(1);
});
it('should check the summary contains the order vat', async() => {
const result = await page.waitToGetProperty(selectors.orderSummary.vat, 'innerText');
expect(result.length).toBeGreaterThan(1);
});
it('should check the summary contains the order total', async() => {
const result = await page.waitToGetProperty(selectors.orderSummary.total, 'innerText');
expect(result.length).toBeGreaterThan(1);
});
it('should check the summary contains the order sales', async() => {
const result = await page.countElement(selectors.orderSummary.sale);
expect(result).toBeGreaterThan(0);
});
});

View File

@ -0,0 +1,102 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Travel basic data path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('buyer', 'travel');
await page.accessToSearchResult('3');
await page.accessToSection('travel.card.basicData');
});
afterAll(async() => {
await browser.close();
});
it('should reach the thermograph section', async() => {
const result = await page.expectURL('/basic-data');
expect(result).toBe(true);
});
it('should set a wrong delivery date then receive an error on submit', async() => {
await page.datePicker(selectors.travelBasicDada.deliveryDate, -1, null);
await page.waitToClick(selectors.travelBasicDada.save);
const result = await page.waitForLastSnackbar();
expect(result).toEqual('Landing cannot be lesser than shipment');
});
it('should undo the changes', async() => {
await page.waitToClick(selectors.travelBasicDada.undoChanges);
await page.waitToClick(selectors.travelBasicDada.save);
const result = await page.waitForLastSnackbar();
expect(result).toEqual('No changes to save');
});
it('should now edit the whole form then save', async() => {
await page.clearInput(selectors.travelBasicDada.reference);
await page.write(selectors.travelBasicDada.reference, 'new reference!');
await page.waitFor(2000);
await page.autocompleteSearch(selectors.travelBasicDada.agency, 'Entanglement');
await page.autocompleteSearch(selectors.travelBasicDada.outputWarehouse, 'Warehouse Three');
await page.autocompleteSearch(selectors.travelBasicDada.inputWarehouse, 'Warehouse Four');
await page.waitToClick(selectors.travelBasicDada.delivered);
await page.waitToClick(selectors.travelBasicDada.received);
await page.waitToClick(selectors.travelBasicDada.save);
const result = await page.waitForLastSnackbar();
expect(result).toEqual('Data saved!');
});
it('should reload the section and check the reference was saved', async() => {
await page.reloadSection('travel.card.basicData');
const result = await page.waitToGetProperty(selectors.travelBasicDada.reference, 'value');
expect(result).toEqual('new reference!');
});
it('should check the agency was saved', async() => {
const result = await page.waitToGetProperty(selectors.travelBasicDada.agency, 'value');
expect(result).toEqual('Entanglement');
});
it('should check the output warehouse date was saved', async() => {
const result = await page.waitToGetProperty(selectors.travelBasicDada.outputWarehouse, 'value');
expect(result).toEqual('Warehouse Three');
});
it('should check the input warehouse date was saved', async() => {
const result = await page.waitToGetProperty(selectors.travelBasicDada.inputWarehouse, 'value');
expect(result).toEqual('Warehouse Four');
});
it(`should check the delivered checkbox was saved even tho it doesn't make sense`, async() => {
await page.waitForClassPresent(selectors.travelBasicDada.delivered, 'checked');
});
it(`should check the received checkbox was saved even tho it doesn't make sense`, async() => {
await page.waitForClassPresent(selectors.travelBasicDada.received, 'checked');
});
it('should navigate to the travel logs', async() => {
await page.accessToSection('travel.card.log');
const result = await page.expectURL('/log');
expect(result).toBe(true);
});
it('should check the 1st log contains details from the changes made', async() => {
const result = await page.waitToGetProperty(selectors.travelLog.firstLogFirstTD, 'innerText');
expect(result).toContain('new reference!');
});
});

View File

@ -0,0 +1,61 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Entry descriptor path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('buyer', 'entry');
await page.accessToSearchResult('2');
});
afterAll(async() => {
await browser.close();
});
it('should reach the second entry summary section', async() => {
let url = await page.expectURL('#!/entry/2/summary');
expect(url).toBe(true);
});
it('should show some entry information', async() => {
const result = await page.waitToGetProperty(selectors.entryDescriptor.agency, 'innerText');
expect(result).toContain('inhouse pickup');
});
it('should click the travels button to be redirected to the travels index filtered by the current agency', async() => {
await page.waitToClick(selectors.entryDescriptor.travelsQuicklink);
const url = await page.expectURL('/travel/index');
const filter = await page.expectURL('agencyFk');
expect(url).toBe(true);
expect(filter).toBe(true);
});
it('should go back to the entry summary', async() => {
await page.waitToClick(selectors.globalItems.homeButton);
await page.selectModule('entry');
await page.accessToSearchResult('2');
let url = await page.expectURL('#!/entry/2/summary');
expect(url).toBe(true);
});
it('should click the entries button to be redirected to the entries index filtered by the current supplier', async() => {
await page.waitToClick(selectors.entryDescriptor.entriesQuicklink);
const url = await page.expectURL('/entry/index');
const supplierFilter = await page.expectURL('supplierFk');
const toFilter = await page.expectURL('to');
const fromFilter = await page.expectURL('from');
expect(url).toBe(true);
expect(supplierFilter).toBe(true);
expect(toFilter).toBe(true);
expect(fromFilter).toBe(true);
});
});

View File

@ -30,7 +30,7 @@
ng-click="$ctrl.onClear($event)">
</vn-icon>
<vn-icon
icon="cloud_upload"
icon="attach_file"
vn-tooltip="Select a file"
ng-click="$ctrl.openFileSelector()">
</vn-icon>

View File

@ -16,7 +16,7 @@ describe('Client isValidClient', () => {
});
it('should call the isValidClient() method with an unexistant id and receive false', async() => {
let id = 999999;
let id = 999;
let result = await app.models.Client.isValidClient(id);
expect(result).toBeFalsy();

View File

@ -56,7 +56,16 @@
label="File"
ng-model="$ctrl.dms.files"
on-change="$ctrl.onFileChange($files)"
accept=".pdf, .png, .jpg, .jpeg, application/zip, application/rar, application/x-7z-compressed">
accept="{{$ctrl.allowedContentTypes}}"
required="true"
multiple="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
<vn-vertical>

View File

@ -53,11 +53,6 @@
{{::document.dms.description}}
</span>
</vn-td>
<vn-td shrink>
<vn-check disabled="true"
ng-model="document.dms.hasFile">
</vn-check>
</vn-td>
<vn-td shrink>
<a target="_blank"
title="{{'Download file' | translate}}"

View File

@ -34,7 +34,7 @@ describe('ticket-request confirm()', () => {
expect(error.message).toEqual(`That item doesn't exists`);
});
it(`should throw an error if the item is not available`, async() => {
it('should throw an error if the item is not available', async() => {
const requestId = 5;
const itemId = 4;
const quantity = 99999;

View File

@ -67,6 +67,10 @@ module.exports = Self => {
arg: 'problems',
type: 'Boolean',
description: `Whether to show only tickets with problems`
}, {
arg: 'pending',
type: 'Boolean',
description: `Whether to show only tickets with state 'Pending'`
}, {
arg: 'mine',
type: 'Boolean',
@ -130,7 +134,7 @@ module.exports = Self => {
dateTo.setHours(23, 59, 0, 0);
}
let where = buildFilter(ctx.args, (param, value) => {
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
@ -155,6 +159,17 @@ module.exports = Self => {
return {'c.salesPersonFk': {inq: teamIds}};
case 'alertLevel':
return {'ts.alertLevel': value};
case 'pending':
if (value) {
return {and: [
{'st.alertLevel': 0},
{'st.code': {neq: 'OK'}}
]};
} else {
return {and: [
{'st.alertLevel': {gt: 0}}
]};
}
case 'id':
case 'clientFk':
case 'agencyModeFk':
@ -244,7 +259,6 @@ module.exports = Self => {
LEFT JOIN tmp.ticketProblems tp ON tp.ticketFk = f.id
LEFT JOIN tmp.ticketTotal tt ON tt.ticketFk = f.id`);
let condition;
let hasProblem;
let range;

View File

@ -32,7 +32,7 @@ module.exports = Self => {
let alertLevel = state ? state.alertLevel : null;
let ticket = await Self.app.models.Ticket.findById(id, {
fields: ['isDeleted', 'clientFk', 'refFk'],
fields: ['clientFk'],
include: [{
relation: 'client',
scope: {
@ -42,13 +42,13 @@ module.exports = Self => {
}
}]
});
const isLocked = await Self.app.models.Ticket.isLocked(id);
const isDeleted = ticket && ticket.isDeleted;
const isOnDelivery = (alertLevel && alertLevel > 0);
const alertLevelGreaterThanZero = (alertLevel && alertLevel > 0);
const isNormalClient = ticket && ticket.client().type().code == 'normal';
const isInvoiced = ticket && ticket.refFk;
const validAlertAndRoleNormalClient = (alertLevelGreaterThanZero && isNormalClient && !isValidRole);
if (!ticket || isInvoiced || isDeleted || (isOnDelivery && isNormalClient && !isValidRole))
if (!ticket || validAlertAndRoleNormalClient || isLocked)
return false;
return true;

View File

@ -0,0 +1,35 @@
module.exports = Self => {
Self.remoteMethod('isLocked', {
description: 'Check if a ticket is invoiced or deleted',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'the ticket id',
http: {source: 'path'}
}],
returns: {
type: 'boolean',
root: true
},
http: {
path: `/:id/isLocked`,
verb: 'get'
}
});
Self.isLocked = async id => {
const ticket = await Self.app.models.Ticket.findById(id, {
fields: ['isDeleted', 'refFk']
});
const isDeleted = ticket && ticket.isDeleted;
const isInvoiced = ticket && ticket.refFk;
if (!ticket || isInvoiced || isDeleted)
return true;
return false;
};
};

View File

@ -41,6 +41,34 @@ describe('ticket filter()', () => {
const firstRow = result[0];
expect(result.length).toEqual(1);
expect(firstRow.ticketFk).toEqual(11);
expect(firstRow.id).toEqual(11);
});
it('should return the tickets with grouped state "Pending" and not "Ok"', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {pending: true}};
const filter = {};
const result = await app.models.Ticket.filter(ctx, filter);
const firstRow = result[0];
const secondRow = result[1];
const thirdRow = result[2];
expect(result.length).toEqual(3);
expect(firstRow.state).toEqual('Arreglar');
expect(secondRow.state).toEqual('Arreglar');
expect(thirdRow.state).toEqual('Arreglar');
});
it('should return the tickets that are not pending', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {pending: false}};
const filter = {};
const result = await app.models.Ticket.filter(ctx, filter);
const firstRow = result[0];
const secondRow = result[1];
const thirdRow = result[2];
expect(result.length).toEqual(13);
expect(firstRow.state).toEqual('Entregado');
expect(secondRow.state).toEqual('Entregado');
expect(thirdRow.state).toEqual('Entregado');
});
});

View File

@ -1,13 +1,6 @@
const app = require('vn-loopback/server/server');
describe('ticket isEditable()', () => {
it('should return false if the given ticket is not editable', async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let result = await app.models.Ticket.isEditable(ctx, 2);
expect(result).toEqual(false);
});
it('should return false if the given ticket does not exist', async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let result = await app.models.Ticket.isEditable(ctx, 99999);
@ -15,37 +8,46 @@ describe('ticket isEditable()', () => {
expect(result).toEqual(false);
});
it('should return false if the given ticket isDeleted', async() => {
it(`should return false if the given ticket isn't invoiced but isDeleted`, async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let result = await app.models.Ticket.isEditable(ctx, 19);
let deletedTicket = await app.models.Ticket.findOne({
where: {
invoiceOut: null,
isDeleted: true
},
fields: ['id']
});
let result = await app.models.Ticket.isEditable(ctx, deletedTicket.id);
expect(result).toEqual(false);
});
it('should return true if the given ticket is editable', async() => {
let ctx = {req: {accessToken: {userId: 9}}};
let result = await app.models.Ticket.isEditable(ctx, 16);
expect(result).toEqual(true);
});
it('should be able to edit a deleted or invoiced ticket if the role is salesAssistant', async() => {
it('should not be able to edit a deleted or invoiced ticket even for salesAssistant', async() => {
let ctx = {req: {accessToken: {userId: 21}}};
let result = await app.models.Ticket.isEditable(ctx, 8);
let result = await app.models.Ticket.isEditable(ctx, 19);
expect(result).toEqual(true);
expect(result).toEqual(false);
});
it('should be able to edit a deleted or invoiced ticket if the role is productionBoss', async() => {
it('should not be able to edit a deleted or invoiced ticket even for productionBoss', async() => {
let ctx = {req: {accessToken: {userId: 50}}};
let result = await app.models.Ticket.isEditable(ctx, 8);
let result = await app.models.Ticket.isEditable(ctx, 19);
expect(result).toEqual(true);
expect(result).toEqual(false);
});
it('should not be able to edit a deleted or invoiced ticket if the role is salesPerson', async() => {
it('should not be able to edit a deleted or invoiced ticket even for salesPerson', async() => {
let ctx = {req: {accessToken: {userId: 18}}};
let result = await app.models.Ticket.isEditable(ctx, 8);
let result = await app.models.Ticket.isEditable(ctx, 19);
expect(result).toEqual(false);
});

View File

@ -0,0 +1,34 @@
const app = require('vn-loopback/server/server');
describe('ticket isLocked()', () => {
it('should return true if the given ticket does not exist', async() => {
let result = await app.models.Ticket.isLocked(99999);
expect(result).toEqual(true);
});
it('should return true if the given ticket is invoiced', async() => {
let invoicedTicket = await app.models.Ticket.findOne({
where: {invoiceOut: {neq: null}},
fields: ['id']
});
let result = await app.models.Ticket.isLocked(invoicedTicket.id);
expect(result).toEqual(true);
});
it(`should return true if the given ticket isn't invoiced but deleted`, async() => {
let deletedTicket = await app.models.Ticket.findOne({
where: {
invoiceOut: null,
isDeleted: true
},
fields: ['id']
});
let result = await app.models.Ticket.isLocked(deletedTicket.id);
expect(result).toEqual(true);
});
});

View File

@ -35,6 +35,7 @@ module.exports = Self => {
});
Self.updateDiscount = async(ctx, id, salesIds, newDiscount) => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
const tx = await Self.beginTransaction({});
@ -68,8 +69,14 @@ module.exports = Self => {
if (!allFromSameTicket)
throw new UserError('All sales must belong to the same ticket');
const isEditable = await models.Ticket.isEditable(ctx, id);
if (!isEditable)
const isLocked = await models.Ticket.isLocked(id);
const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson');
const state = await Self.app.models.TicketState.findOne({
where: {ticketFk: id}
});
const alertLevel = state ? state.alertLevel : null;
if (isLocked || (!isSalesPerson && alertLevel > 0 ))
throw new UserError(`The sales of this ticket can't be modified`);
const ticket = await models.Ticket.findById(id, {

View File

@ -29,6 +29,7 @@ module.exports = Self => {
require('../methods/ticket/recalculateComponents')(Self);
require('../methods/ticket/deleteStowaway')(Self);
require('../methods/ticket/sendSms')(Self);
require('../methods/ticket/isLocked')(Self);
Self.observe('before save', async function(ctx) {
if (ctx.isNewInstance) return;

View File

@ -51,11 +51,6 @@
{{::document.dms.description}}
</span>
</vn-td>
<vn-td shrink>
<vn-check disabled="true"
field="document.dms.hasFile">
</vn-check>
</vn-td>
<vn-td shrink>
<a target="_blank"
title="{{'Download file' | translate}}"

View File

@ -165,8 +165,8 @@
</span>
</vn-td>
<vn-td number>
<span ng-class="{'link': $ctrl.isEditable}"
title="{{$ctrl.isEditable ? 'Edit discount' : ''}}"
<span ng-class="{'link': !$ctrl.isLocked}"
title="{{!$ctrl.isLocked ? 'Edit discount' : ''}}"
ng-click="$ctrl.showEditDiscountPopover($event, sale)">
{{(sale.discount / 100) | percentage}}
</span>

View File

@ -46,6 +46,7 @@ class Controller {
set ticket(value) {
this._ticket = value;
this.isTicketEditable();
this.isTicketLocked();
}
get sales() {
@ -354,7 +355,7 @@ class Controller {
}
showEditDiscountPopover(event, sale) {
if (!this.isEditable) return;
if (this.isLocked) return;
this.sale = sale;
this.edit = [{
@ -540,6 +541,12 @@ class Controller {
});
}
isTicketLocked() {
this.$http.get(`Tickets/${this.$state.params.id}/isLocked`).then(res => {
this.isLocked = res.data;
});
}
hasOneSaleSelected() {
if (this.totalCheckedLines() === 1)
return true;

View File

@ -69,6 +69,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
$httpBackend.when('POST', `Claims/createFromSales`, {claim: claim, sales: sales}).respond(claim);
$httpBackend.expect('POST', `Claims/createFromSales`).respond(claim);
controller.createClaim();
@ -98,6 +99,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
let result = controller.checkedLines();
$httpBackend.flush();
@ -116,6 +118,7 @@ describe('Ticket', () => {
$httpBackend.expectGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.expectGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.onStateOkClick();
$httpBackend.flush();
@ -129,6 +132,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.onStateChange(3);
$httpBackend.flush();
});
@ -142,6 +146,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.onRemoveLinesClick('accept');
$httpBackend.flush();
@ -183,6 +188,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.unmarkAsReserved(false);
$httpBackend.flush();
});
@ -213,6 +219,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.updateQuantity(sale);
$httpBackend.flush();
@ -232,6 +239,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.updateConcept(sale);
$httpBackend.flush();
@ -262,6 +270,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.addSale(newSale);
$httpBackend.flush();
@ -287,6 +296,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.transferSales(13);
$httpBackend.flush();
@ -305,6 +315,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.setTransferParams();
$httpBackend.flush();
@ -330,6 +341,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.newOrderFromTicket();
$httpBackend.flush();
@ -353,6 +365,7 @@ describe('Ticket', () => {
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
$httpBackend.whenGET(`Tickets/1/isLocked`).respond();
controller.calculateSalePrice();
$httpBackend.flush();

View File

@ -113,10 +113,16 @@
</vn-check>
<vn-check
vn-one
label="Problems"
label="With problems"
ng-model="filter.problems"
triple-state="true">
</vn-check>
<vn-check
vn-one
label="Pending"
ng-model="filter.pending"
triple-state="true">
</vn-check>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>

View File

@ -11,3 +11,5 @@ My team: Mi equipo
Order id: Id pedido
Grouped States: Estado agrupado
Days onward: Días adelante
With problems: Con problemas
Pending: Pendientes

View File

@ -14,6 +14,10 @@ module.exports = Self => {
type: 'String',
description: 'The thermograph id',
required: true
}, {
arg: 'state',
type: 'String',
required: true
}, {
arg: 'warehouseId',
type: 'Number',
@ -48,13 +52,12 @@ module.exports = Self => {
}
});
Self.createThermograph = async(ctx, id, thermographId) => {
Self.createThermograph = async(ctx, id, thermographId, state) => {
const models = Self.app.models;
const tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
const travelThermograph = await models.TravelThermograph.findOne({
where: {
thermographFk: thermographId,
@ -70,7 +73,8 @@ module.exports = Self => {
await travelThermograph.updateAttributes({
dmsFk: firstDms.id,
travelFk: id
travelFk: id,
result: state
}, options);
await tx.commit();

View File

@ -0,0 +1,83 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('updateThermograph', {
description: 'updates a file properties or file',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The travel id',
http: {source: 'path'}
}, {
arg: 'thermographId',
type: 'String',
description: 'The thermograph id',
required: true
}, {
arg: 'state',
type: 'String',
required: true
}, {
arg: 'warehouseId',
type: 'Number',
description: 'The warehouse id'
}, {
arg: 'companyId',
type: 'Number',
description: 'The company id'
}, {
arg: 'dmsTypeId',
type: 'Number',
description: 'The dms type id'
}, {
arg: 'reference',
type: 'String'
}, {
arg: 'description',
type: 'String'
}, {
arg: 'hasFileAttached',
type: 'Boolean',
description: 'True if has an attached file'
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/updateThermograph`,
verb: 'POST'
}
});
Self.updateThermograph = async(ctx, id, thermographId, state) => {
const models = Self.app.models;
const tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
const travelThermograph = await models.TravelThermograph.findOne({
where: {
thermographFk: thermographId,
travelFk: id
}
}, options);
if (!travelThermograph)
throw new UserError('No valid travel thermograph found');
const dmsFk = travelThermograph.dmsFk;
await models.Dms.updateFile(ctx, dmsFk, options);
await travelThermograph.updateAttributes({
result: state
}, options);
await tx.commit();
return travelThermograph;
} catch (e) {
await tx.rollback();
throw e;
}
};
};

View File

@ -4,4 +4,5 @@ module.exports = Self => {
require('../methods/travel/filter')(Self);
require('../methods/travel/createThermograph')(Self);
require('../methods/travel/deleteThermograph')(Self);
require('../methods/travel/updateThermograph')(Self);
};

View File

@ -11,4 +11,5 @@ import './log';
import './create';
import './thermograph/index/';
import './thermograph/create/';
import './thermograph/edit/';
import './descriptor-popover';

View File

@ -81,6 +81,15 @@
"travel": "$ctrl.travel"
},
"acl": ["buyer"]
}, {
"url" : "/:thermographId/edit",
"state": "travel.card.thermograph.edit",
"component": "vn-travel-thermograph-edit",
"description": "Edit thermograph",
"params": {
"travel": "$ctrl.travel"
},
"acl": ["buyer"]
}
]
}

View File

@ -9,6 +9,35 @@
enctype="multipart/form-data">
<div class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete vn-one
label="Thermograph"
ng-model="$ctrl.dms.thermographId"
url="TravelThermographs"
where="{travelFk: null}"
show-field="thermographFk"
value-field="thermographFk">
</vn-autocomplete>
<vn-textfield vn-one
label="State"
ng-model="$ctrl.dms.state"
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one
label="Reference"
ng-model="$ctrl.dms.reference"
rule>
</vn-textfield>
<vn-autocomplete vn-one
label="Type"
ng-model="$ctrl.dms.dmsTypeId"
url="DmsTypes"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
label="Company"
@ -25,28 +54,6 @@
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
label="Type"
ng-model="$ctrl.dms.dmsTypeId"
url="DmsTypes"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-textfield vn-one
label="Reference"
ng-model="$ctrl.dms.reference"
rule>
</vn-textfield>
<vn-autocomplete vn-one
label="Thermograph"
ng-model="$ctrl.dms.thermographId"
url="TravelThermographs"
where="{travelFk: null}"
show-field="thermographFk"
value-field="thermographFk">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea vn-one vn-focus
label="Description"

View File

@ -8,7 +8,7 @@ class Controller {
this.$translate = $translate;
this.vnApp = vnApp;
this.vnConfig = vnConfig;
this.dms = {files: []};
this.dms = {files: [], state: 'Ok'};
}
get travel() {

View File

@ -0,0 +1,87 @@
<vn-watcher
vn-id="watcher"
data="$ctrl.dms">
</vn-watcher>
<form
name="form"
ng-submit="$ctrl.onSubmit()"
class="vn-ma-md"
enctype="multipart/form-data">
<div class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete vn-one
label="Thermograph"
ng-model="$ctrl.thermograph.thermographId"
url="TravelThermographs"
show-field="thermographFk"
value-field="thermographFk"
disabled="true">
</vn-autocomplete>
<vn-textfield vn-one
label="State"
ng-model="$ctrl.thermograph.state"
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one
label="Reference"
ng-model="$ctrl.thermograph.reference"
rule>
</vn-textfield>
<vn-autocomplete vn-one
label="Type"
ng-model="$ctrl.thermograph.dmsTypeId"
url="DmsTypes"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
label="Company"
ng-model="$ctrl.thermograph.companyId"
url="Companies"
show-field="code"
value-field="id">
</vn-autocomplete>
<vn-autocomplete vn-one
label="Warehouse"
ng-model="$ctrl.thermograph.warehouseId"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea vn-one vn-focus
label="Description"
ng-model="$ctrl.thermograph.description"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-input-file
vn-one
label="File"
ng-model="$ctrl.thermograph.files"
on-change="$ctrl.onFileChange($files)"
accept="{{$ctrl.allowedContentTypes}}"
multiple="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
<vn-button ui-sref="travel.card.thermograph.index" label="Cancel"></vn-button>
</vn-button-bar>
</div>
</form>

View File

@ -0,0 +1,98 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
class Controller extends Component {
get travel() {
return this._travel;
}
set travel(value) {
this._travel = value;
if (value) {
this.setDefaultParams();
this.getAllowedContentTypes();
}
}
getAllowedContentTypes() {
this.$http.get('TravelThermographs/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});
}
get contentTypesInfo() {
return this.$translate.instant('ContentTypesInfo', {
allowedContentTypes: this.allowedContentTypes
});
}
setDefaultParams() {
const filterObj = {include: {relation: 'dms'}};
const filter = encodeURIComponent(JSON.stringify(filterObj));
const path = `TravelThermographs/${this.$params.thermographId}?filter=${filter}`;
this.$http.get(path).then(res => {
const thermograph = res.data && res.data;
this.thermograph = {
thermographId: thermograph.thermographFk,
state: thermograph.result,
reference: thermograph.dms.reference,
warehouseId: thermograph.dms.warehouseFk,
companyId: thermograph.dms.companyFk,
dmsTypeId: thermograph.dms.dmsTypeFk,
description: thermograph.dms.description,
hasFile: thermograph.dms.hasFile,
hasFileAttached: false,
files: []
};
});
}
onSubmit() {
const query = `travels/${this.$params.id}/updateThermograph`;
const options = {
method: 'POST',
url: query,
params: this.thermograph,
headers: {
'Content-Type': undefined
},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.thermograph.files
};
this.$http(options).then(res => {
if (res) {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
this.$.watcher.updateOriginalData();
this.$state.go('travel.card.thermograph.index');
}
});
}
onFileChange(files) {
let hasFileAttached = false;
if (files.length > 0)
hasFileAttached = true;
this.$.$applyAsync(() => {
this.thermograph.hasFileAttached = hasFileAttached;
});
}
}
ngModule.component('vnTravelThermographEdit', {
template: require('./index.html'),
controller: Controller,
bindings: {
travel: '<'
}
});

View File

@ -0,0 +1,122 @@
import './index';
import watcher from 'core/mocks/watcher.js';
describe('Worker', () => {
describe('Component vnTravelThermographEdit', () => {
let controller;
let $scope;
let $element;
let $httpBackend;
let $httpParamSerializer;
beforeEach(ngModule('travel'));
beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
$element = angular.element(`<vn-travel-thermograph-edit></vn-travel-thermograph-edit`);
controller = $componentController('vnTravelThermographEdit', {$element, $scope});
controller._travel = {id: 3};
controller.$params = {id: 3, thermographId: 6};
controller.$.watcher = watcher;
}));
describe('travel() setter', () => {
it('should set the travel data and then call setDefaultParams() and getAllowedContentTypes()', () => {
jest.spyOn(controller, 'setDefaultParams');
jest.spyOn(controller, 'getAllowedContentTypes');
controller._travel = undefined;
controller.travel = {
id: 3
};
expect(controller.setDefaultParams).toHaveBeenCalledWith();
expect(controller.travel).toBeDefined();
expect(controller.getAllowedContentTypes).toHaveBeenCalledWith();
});
});
describe('setDefaultParams()', () => {
it('should perform a GET query and define the dms property on controller', () => {
const thermographId = 6;
const expectedResponse = {
thermographFk: 6,
result: 'Ok',
dms: {
reference: '123456-01',
warehouseFk: 1,
companyFk: 442,
dmsTypeFk: 3,
description: 'Test'
}
};
const filterObj = {include: {relation: 'dms'}};
const filter = encodeURIComponent(JSON.stringify(filterObj));
const query = `TravelThermographs/${thermographId}?filter=${filter}`;
$httpBackend.expect('GET', query).respond(expectedResponse);
controller.setDefaultParams();
$httpBackend.flush();
expect(controller.thermograph).toBeDefined();
expect(controller.thermograph.reference).toEqual('123456-01');
expect(controller.thermograph.dmsTypeId).toEqual(3);
expect(controller.thermograph.state).toEqual('Ok');
});
});
describe('onFileChange()', () => {
it('should set dms hasFileAttached property to true if has any files', () => {
const files = [{id: 1, name: 'MyFile'}];
controller.thermograph = {hasFileAttached: false};
controller.onFileChange(files);
$scope.$apply();
expect(controller.thermograph.hasFileAttached).toBeTruthy();
});
});
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.when('GET', `TravelThermographs/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `TravelThermographs/allowedContentTypes`);
controller.getAllowedContentTypes();
$httpBackend.flush();
expect(controller.allowedContentTypes).toBeDefined();
expect(controller.allowedContentTypes).toEqual('image/png, image/jpg');
});
});
describe('contentTypesInfo()', () => {
it('should return a description with a list of allowed content types', () => {
controller.allowedContentTypes = ['image/png', 'image/jpg'];
const expectedTypes = controller.allowedContentTypes.join(', ');
const expectedResult = `Allowed content types: ${expectedTypes}`;
jest.spyOn(controller.$translate, 'instant').mockReturnValue(expectedResult);
const result = controller.contentTypesInfo;
expect(result).toEqual(expectedResult);
});
});
describe('onSubmit()', () => {
it('should make an HTTP POST request to save the form data', () => {
jest.spyOn(controller.$.watcher, 'updateOriginalData');
const files = [{id: 1, name: 'MyFile'}];
controller.thermograph = {files};
const serializedParams = $httpParamSerializer(controller.thermograph);
const query = `travels/${controller.$params.id}/updateThermograph?${serializedParams}`;
$httpBackend.expect('POST', query).respond({});
controller.onSubmit();
$httpBackend.flush();
});
});
});
});

View File

@ -0,0 +1,7 @@
vn-ticket-request {
.vn-textfield {
margin: 0!important;
max-width: 100px;
}
}

View File

@ -23,11 +23,11 @@
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="thermograph in $ctrl.travelThermographs">
<vn-td>{{thermograph.thermographFk}} </vn-td>
<vn-td>{{thermograph.temperature}} </vn-td>
<vn-td expand>{{thermograph.result}}</vn-td>
<vn-td>{{thermograph.warehouse.name}}</vn-td>
<vn-td>{{thermograph.created | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td>{{::thermograph.thermographFk}} </vn-td>
<vn-td>{{::thermograph.temperature}} </vn-td>
<vn-td expand>{{::thermograph.result}}</vn-td>
<vn-td>{{::thermograph.warehouse.name}}</vn-td>
<vn-td>{{::thermograph.created | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td shrink>
<a target="_blank"
href="api/dms/{{::thermograph.dmsFk}}/downloadFile?access_token={{::$ctrl.accessToken}}">
@ -37,6 +37,12 @@
</vn-icon-button>
</a>
</vn-td>
<vn-td shrink>
<vn-icon-button ui-sref="travel.card.thermograph.edit({thermographId: {{::thermograph.id}}})"
icon="edit"
title="{{'Edit file' | translate}}">
</vn-icon-button>
</vn-td>
<vn-td shrink>
<vn-icon-button
icon="delete"

View File

@ -12,6 +12,7 @@ FileDescription: Travel id {{travelId}}
ContentTypesInfo: 'Tipos de archivo permitidos: {{allowedContentTypes}}'
Are you sure you want to continue?: ¿Seguro que quieres continuar?
Add thermograph: Añadir termógrafo
Edit thermograph: Editar termógrafo
Thermograph deleted: Termógrafo eliminado
Thermograph: Termógrafo
Are you sure you want to remove the thermograph?: ¿Seguro que quieres quitar el termógrafo?

View File

@ -5,7 +5,7 @@ module.exports = Self => {
accepts: {
arg: 'id',
type: 'Number',
description: 'The document id',
description: 'The worker document id',
http: {source: 'path'}
},
returns: {

View File

@ -13,10 +13,10 @@
},
"properties": {
"id": {
"type": "Number"
"type": "Number",
"id": true
},
"dmsFk": {
"id": true,
"type": "Number",
"required": true,
"mysql": {

View File

@ -19,7 +19,6 @@
<vn-th field="reference" shrink>Reference</vn-th>
<vn-th expand>Description</vn-th>
<vn-th field="hasFile" shrink>Original</vn-th>
<vn-th shrink>File</vn-th>
<vn-th field="created">Created</vn-th>
<vn-th shrink></vn-th>
<vn-th shrink></vn-th>
@ -39,17 +38,13 @@
{{::document.dms.description}}
</span>
</vn-td>
<vn-td shrink>
<vn-check disabled="true"
ng-model="document.dms.hasFile">
</vn-check>
</vn-td>
<vn-td shrink>
<a target="_blank"
title="{{'Download file' | translate}}"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.accessToken}}">{{::document.dms.file}}
</a>
</vn-td>
<vn-td>
{{::document.dms.created | date:'dd/MM/yyyy HH:mm'}}
</vn-td>
<vn-td shrink>

View File

@ -58,8 +58,8 @@ class Controller extends Component {
deleteDms(response) {
if (response === 'accept') {
const dmsFk = this.workerDms[this.dmsIndex].dmsFk;
const query = `WorkerDms/${dmsFk}/removeFile`;
const workerDmsId = this.workerDms[this.dmsIndex].id;
const query = `WorkerDms/${workerDmsId}/removeFile`;
this.$http.post(query).then(() => {
this.$.model.remove(this.dmsIndex);
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));

View File

@ -22,15 +22,15 @@ describe('Worker', () => {
describe('deleteDms()', () => {
it('should make an HTTP Post query', () => {
const dmsId = 4;
const workerDmsId = 1;
const dmsIndex = 0;
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$.model, 'remove');
controller.workerDms = [{dmsFk: 4}];
controller.workerDms = [{id: 1, dmsFk: 4}];
controller.dmsIndex = dmsIndex;
$httpBackend.when('POST', `WorkerDms/${dmsId}/removeFile`).respond({});
$httpBackend.expect('POST', `WorkerDms/${dmsId}/removeFile`);
$httpBackend.when('POST', `WorkerDms/${workerDmsId}/removeFile`).respond({});
$httpBackend.expect('POST', `WorkerDms/${workerDmsId}/removeFile`);
controller.deleteDms('accept');
$httpBackend.flush();

View File

@ -157,6 +157,22 @@ table {
border-spacing: 0;
}
/**
* Prevent page break fix
*/
tbody {
page-break-inside: avoid;
break-inside: avoid;
display: block;
width: 100%
}
thead, tbody tr {
table-layout: fixed;
display: table;
width: 100%;
}
.row-oriented, .column-oriented {
text-align: left;
width: 100%
@ -181,6 +197,10 @@ table {
background-color: #e5e5e5
}
.column-oriented tbody {
border-bottom: 1px solid #DDD;
}
.column-oriented tfoot {
border-top: 2px solid #808080;
}
@ -190,7 +210,6 @@ table {
}
.column-oriented .description {
border-bottom: 1px solid #DDD;
font-size: 0.8em
}

View File

@ -42,6 +42,7 @@
font-weight: bold
}
.non-page-break {
.no-page-break {
page-break-inside: avoid;
break-inside: avoid
}

View File

@ -59,37 +59,35 @@
<tr>
<th>{{$t('Code')}}</th>
<th class="number">{{$t('Quantity')}}</th>
<th>{{$t('Concept')}}</th>
<th width="50%">{{$t('Concept')}}</th>
</tr>
</thead>
<tbody>
<template v-for="sale in sales">
<tr class="font bold">
<td>{{sale.itemFk}}</td>
<td class="number">{{Math.trunc(sale.subtotal)}}</td>
<td>{{sale.concept}}</td>
</tr>
<tr class="description">
<td class="centered">
<div v-if="sale.value5">
<strong class="font gray">{{sale.tag5}}</strong>
<span>{{sale.value5}}</span>
</div>
</td>
<td class="centered">
<div v-if="sale.value6">
<strong class="font gray">{{sale.tag6}}</strong>
<span>{{sale.value6}}</span>
</div>
</td>
<td class="centered">
<div v-if="sale.value7">
<strong class="font gray">{{sale.tag7}}</strong>
<span>{{sale.value7}}</span>
</div>
</td>
</tr>
</template>
<tbody v-for="sale in sales">
<tr class="font bold">
<td>{{sale.itemFk}}</td>
<td class="number">{{Math.trunc(sale.subtotal)}}</td>
<td width="50%">{{sale.concept}}</td>
</tr>
<tr class="description">
<td>
<div v-if="sale.value5">
<strong class="font gray">{{sale.tag5}}</strong>
<span>{{sale.value5}}</span>
</div>
</td>
<td class="centered">
<div v-if="sale.value6">
<strong class="font gray">{{sale.tag6}}</strong>
<span>{{sale.value6}}</span>
</div>
</td>
<td class="align-right">
<div v-if="sale.value7">
<strong class="font gray">{{sale.tag7}}</strong>
<span>{{sale.value7}}</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -64,8 +64,8 @@
<th>{{$t('concept')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="sale in sales" v-bind:key="sale.id">
<tbody v-for="sale in sales" v-bind:key="sale.id">
<tr>
<td class="font gray">{{sale.id}}</td>
<td class="number">{{sale.quantity}}</td>
<td class="number">{{sale.claimQuantity}}</td>

View File

@ -76,53 +76,48 @@
<tr>
<td>{{$t('reference')}}</td>
<td class="number">{{$t('quantity')}}</td>
<td>{{$t('concept')}}</td>
<td width="50%">{{$t('concept')}}</td>
<td class="number">{{$t('price')}}</td>
<td class="centered">{{$t('discount')}}</td>
<td class="centered">{{$t('vat')}}</td>
<td class="number">{{$t('amount')}}</td>
</tr>
</thead>
<tbody>
<template v-for="sale in sales">
<tr class="font bold">
<td>{{sale.itemFk}}</td>
<td class="number">{{sale.quantity}}</td>
<td>{{sale.concept}}</td>
<td class="number">{{sale.price | currency('EUR', locale)}}</td>
<td class="centered">{{sale.discount | percentage}}</td>
<td class="centered">{{sale.vatType}}</td>
<td class="number">{{sale.price * sale.quantity * (1 - sale.discount / 100) | currency('EUR', locale)}}</td>
</tr>
<tr class="description">
<td colspan="2" class="centered">
<div v-if="sale.value5">
<strong class="font gray">{{sale.tag5}}</strong>
<span>{{sale.value5}}</span>
</div>
</td>
<td colspan="3" class="centered">
<div v-if="sale.value6">
<strong class="font gray">{{sale.tag6}}</strong>
<span>{{sale.value6}}</span>
</div>
</td>
<td colspan="2" class="centered">
<div v-if="sale.value7">
<strong class="font gray">{{sale.tag7}}</strong>
<span>{{sale.value7}}</span>
</div>
</td>
</tr>
<tr class="description phytosanitary" v-if="sale.passportNumber">
<td colspan="7">
{{sale.ediBotanic}} {{sale.denomination}} {{sale.countryCode}}-{{sale.passportNumber}}
<span v-if="sale.isProtectedZone">ZP</span>
</td>
</tr>
</template>
<tr >
<td colspan="7"></td>
<tbody v-for="sale in sales">
<tr class="font bold">
<td>{{sale.itemFk}}</td>
<td class="number">{{sale.quantity}}</td>
<td width="50%">{{sale.concept}}</td>
<td class="number">{{sale.price | currency('EUR', locale)}}</td>
<td class="centered">{{(sale.discount / 100) | percentage}}</td>
<td class="centered">{{sale.vatType}}</td>
<td class="number">{{sale.price * sale.quantity * (1 - sale.discount / 100) | currency('EUR', locale)}}</td>
</tr>
<tr class="description">
<td colspan="2" class="centered">
<div v-if="sale.value5">
<strong class="font gray">{{sale.tag5}}</strong>
<span>{{sale.value5}}</span>
</div>
</td>
<td colspan="3" class="centered">
<div v-if="sale.value6">
<strong class="font gray">{{sale.tag6}}</strong>
<span>{{sale.value6}}</span>
</div>
</td>
<td colspan="2" class="centered">
<div v-if="sale.value7">
<strong class="font gray">{{sale.tag7}}</strong>
<span>{{sale.value7}}</span>
</div>
</td>
</tr>
<tr class="description phytosanitary" v-if="sale.passportNumber">
<td colspan="7">
{{sale.ediBotanic}} {{sale.denomination}} {{sale.countryCode}}-{{sale.passportNumber}}
<span v-if="sale.isProtectedZone">ZP</span>
</td>
</tr>
</tbody>
<tfoot>
@ -138,7 +133,7 @@
<div class="columns">
<!-- Services block-->
<div class="size100" v-if="services.length > 0">
<div class="size100 no-page-break" v-if="services.length > 0">
<h3>{{$t('services')}}</h3>
<table class="column-oriented">
<thead>
@ -168,29 +163,35 @@
<!-- End of services block -->
<!-- Taxes block -->
<div id="taxes" class="size50 pull-right" v-if="taxes">
<div id="taxes" class="size50 pull-right no-page-break" v-if="taxes">
<h3>{{$t('taxBreakdown')}}</h3>
<table class="column-oriented">
<thead>
<tr>
<td>{{$t('type')}}</td>
<td class="number">{{$t('taxBase')}}</td>
<td width="45%">{{$t('type')}}</td>
<td width="20%" class="number">
{{$t('taxBase')}}
</td>
<td>{{$t('tax')}}</td>
<td class="number">{{$t('fee')}}</td>
</tr>
</thead>
<tbody>
<tr v-for="tax in taxes">
<td>{{tax.name}}</td>
<td class="number">{{tax.Base | currency('EUR', locale)}}</td>
<td width="45%">{{tax.name}}</td>
<td width="20%" class="number">
{{tax.Base | currency('EUR', locale)}}
</td>
<td>{{tax.vatPercent | percentage}}</td>
<td class="number">{{tax.tax | currency('EUR', locale)}}</td>
</tr>
</tbody>
<tfoot>
<tr class="font bold">
<td>{{$t('subtotal')}}</td>
<td class="number">{{getTotalBase() | currency('EUR', locale)}}</td>
<td width="45%">{{$t('subtotal')}}</td>
<td width="20%" class="number">
{{getTotalBase() | currency('EUR', locale)}}
</td>
<td></td>
<td class="number">{{getTotalTax()| currency('EUR', locale)}}</td>
</tr>
@ -204,7 +205,7 @@
<!-- End of taxes block -->
<!-- Packages block -->
<div id="packagings" class="size100" v-if="packagings.length > 0">
<div id="packagings" class="size100 no-page-break" v-if="packagings.length > 0">
<h3>{{$t('packagings')}}</h3>
<table class="column-oriented">
<thead>
@ -226,7 +227,7 @@
<!-- End of packages block -->
<!-- Signature block -->
<div class="size50 pull-left">
<div class="size50 pull-left no-page-break">
<div id="signature" class="panel" v-if="signature && signature.id">
<div class="header">{{$t('digitalSignature')}}</div>
<div class="body centered">

View File

@ -84,14 +84,14 @@
</div>
</div>
</div>
<div class="non-page-break" v-for="ticket in tickets">
<div class="no-page-break" v-for="ticket in tickets">
<div>
<table class="column-oriented repeatable">
<thead>
<tr>
<td class="number">{{$t('order')}}</td>
<td class="number">{{$t('ticket')}}</td>
<td>{{$t('client')}}</td>
<td width="50%">{{$t('client')}}</td>
<td class="number">{{$t('address')}}</td>
<td class="number">{{$t('packages')}}</td>
</tr>
@ -100,7 +100,7 @@
<tr>
<td class="number">{{ticket.priority}}</td>
<td class="number">{{ticket.id}}</td>
<td>{{ticket.clientFk}} {{ticket.addressName}}</td>
<td width="50%">{{ticket.clientFk}} {{ticket.addressName}}</td>
<td v-if="ticket.addressFk" class="number">
{{ticket.addressFk.toString().substr(0, ticket.addressFk.toString().length - 3)}}
<span class="black-container">
@ -141,7 +141,7 @@
</tr>
<tr>
<th class="font gray align-right">{{$t('import')}}</th>
<td>{{ticket.import}}</td>
<td>{{ticket.import | currency('EUR', locale)}}</td>
</tr>
</tbody>
</table>

View File

@ -63,45 +63,43 @@
<table class="column-oriented">
<thead>
<tr>
<td class="number">{{$t('boxes')}}</td>
<td>{{$t('boxes')}}</td>
<td class="number">{{$t('packing')}}</td>
<td>{{$t('concept')}}</td>
<td width="50%">{{$t('concept')}}</td>
<td class="number">{{$t('quantity')}}</td>
<td class="number">{{$t('price')}}</td>
<td class="number">{{$t('amount')}}</td>
</tr>
</thead>
<tbody>
<template v-for="buy in buys">
<tr class="font bold">
<td class="number">{{buy.box}}</td>
<td class="number">{{buy.packing}}</td>
<td>{{buy.itemName}}</td>
<td class="number">{{buy.quantity}}</td>
<td class="number">{{buy.buyingValue | currency('EUR', locale)}}</td>
<td class="number">{{buy.buyingValue * buy.quantity | currency('EUR', locale)}}</td>
</tr>
<tr class="description">
<td colspan="2" class="centered">
<div v-if="buy.value5">
<strong class="font gray">{{buy.tag5}}</strong>
<span>{{buy.value5}}</span>
</div>
</td>
<td colspan="2" class="centered">
<div v-if="buy.value6">
<strong class="font gray">{{buy.tag6}}</strong>
<span>{{buy.value6}}</span>
</div>
</td>
<td colspan="2" class="centered">
<div v-if="buy.value7">
<strong class="font gray">{{buy.tag7}}</strong>
<span>{{buy.value7}}</span>
</div>
</td>
</tr>
</template>
<tbody v-for="buy in buys">
<tr class="font bold">
<td class="number">{{buy.box}}</td>
<td class="number">{{buy.packing}}</td>
<td width="50%">{{buy.itemName}}</td>
<td class="number">{{buy.quantity}}</td>
<td class="number">{{buy.buyingValue | currency('EUR', locale)}}</td>
<td class="number">{{buy.buyingValue * buy.quantity | currency('EUR', locale)}}</td>
</tr>
<tr class="description">
<td colspan="2">
<div v-if="buy.value5">
<strong class="font gray">{{buy.tag5}}</strong>
<span>{{buy.value5}}</span>
</div>
</td>
<td colspan="2" class="centered">
<div v-if="buy.value6">
<strong class="font gray">{{buy.tag6}}</strong>
<span>{{buy.value6}}</span>
</div>
</td>
<td colspan="2" class="align-right">
<div v-if="buy.value7">
<strong class="font gray">{{buy.tag7}}</strong>
<span>{{buy.value7}}</span>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
@ -115,7 +113,7 @@
<!-- End of buy block -->
<div class="columns">
<div class="size50">
<div id="notes" class="panel" v-if="entry.notes">
<div id="notes" class="panel no-page-break" v-if="entry.notes">
<div class="body">
<h3>{{$t('notes')}}</h3>
<div>

View File

@ -25,7 +25,7 @@
<th>{{client.id}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('dated')}}</td>
<td class="font gray uppercase">{{$t('date')}}</td>
<th>{{dated}}</th>
</tr>
</tbody>
@ -61,8 +61,8 @@
<th class="number">{{$t('balance')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="sale in sales" :key="sale.id">
<tbody v-for="sale in sales" :key="sale.id">
<tr>
<td>{{sale.issued | date('%d-%m-%Y')}}</td>
<td>{{sale.ref}}</td>
<td class="number">{{sale.debtOut}}</td>

View File

@ -2,7 +2,7 @@ title: Extracto
claimId: Reclamación
clientId: Cliente
clientData: Datos del cliente
dated: Fecha
date: Fecha
concept: Concepto
invoiced: Facturado
payed: Pagado