Merge branch 'dev' into 3771-ticket.expedition
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Joan Sanchez 2022-05-11 12:47:33 +00:00
commit d39147689d
85 changed files with 1250 additions and 245 deletions

View File

@ -46,7 +46,7 @@ module.exports = Self => {
const {data} = await Self.getUserStatus(recipient.name); const {data} = await Self.getUserStatus(recipient.name);
if (data) { if (data) {
if (data.status === 'offline') { if (data.status === 'offline' || data.status === 'busy') {
// Send message to department room // Send message to department room
const workerDepartment = await models.WorkerDepartment.findById(recipientId, { const workerDepartment = await models.WorkerDepartment.findById(recipientId, {
include: { include: {
@ -58,6 +58,8 @@ module.exports = Self => {
if (channelName) if (channelName)
return Self.send(ctx, `#${channelName}`, `@${recipient.name}${message}`); return Self.send(ctx, `#${channelName}`, `@${recipient.name}${message}`);
else
return Self.send(ctx, `@${recipient.name}`, message);
} else } else
return Self.send(ctx, `@${recipient.name}`, message); return Self.send(ctx, `@${recipient.name}`, message);
} }

View File

@ -28,7 +28,7 @@ module.exports = Self => {
const args = ctx.args; const args = ctx.args;
const models = Self.app.models; const models = Self.app.models;
const sale = await models.Sale.findById(args.saleId,); const sale = await models.Sale.findById(args.saleId);
return await sale.updateAttribute('quantity', args.quantity); return await sale.updateAttribute('quantity', args.quantity);
}; };
}; };

View File

@ -0,0 +1,14 @@
create table `vn`.`invoiceOut_queue`
(
invoiceFk int(10) unsigned not null,
queued datetime default now() not null,
printed datetime null,
`status` VARCHAR(50) default '' null,
constraint invoiceOut_queue_pk
primary key (invoiceFk),
constraint invoiceOut_queue_invoiceOut_id_fk
foreign key (invoiceFk) references invoiceOut (id)
on update cascade on delete cascade
)
comment 'Queue for PDF invoicing';

View File

@ -0,0 +1 @@
Delete file

View File

@ -2283,13 +2283,19 @@ INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `c
INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`) INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`)
VALUES VALUES
(1, 14, '1.txt', 'text/plain', 5, 1, 442, NULL, FALSE, 'Ticket:11', 'Ticket:11 dms for the ticket', CURDATE()), (1, 14, '1.txt', 'text/plain', 5, 1, 442, NULL, FALSE, 'Ticket:11', 'Ticket:11 dms for the ticket', CURDATE()),
(2, 5, '2.txt', 'text/plain', 5, 1, 442, 1, TRUE, 'Client:104', 'Client:104 dms for the client', CURDATE()), (2, 5, '2.txt', 'text/plain', 5, 1, 442, 1, TRUE, 'Client:104', 'Client:104 dms for the client', CURDATE()),
(3, 5, '3.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Client: 104', 'Client:104 readme', CURDATE()), (3, 5, '3.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Client: 104', 'Client:104 readme', CURDATE()),
(4, 3, '4.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Worker: 106', 'Worker:106 readme', CURDATE()), (4, 3, '4.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'Worker: 106', 'Worker:106 readme', CURDATE()),
(5, 5, '5.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'travel: 1', 'dmsForThermograph', CURDATE()), (5, 5, '5.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'travel: 1', 'dmsForThermograph', CURDATE()),
(6, 5, '6.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'NotExists', 'DoesNotExists', CURDATE()); (6, 5, '6.txt', 'text/plain', 5, 1, 442, NULL, TRUE, 'NotExists', 'DoesNotExists', CURDATE()),
(7, 20, '7.jpg', 'image/jpeg', 9, 1, 442, NULL, FALSE, '1', 'TICKET ID DEL CLIENTE BRUCE WAYNE ID 1101', CURDATE()),
(8, 20, '8.mp4', 'video/mp4', 9, 1, 442, NULL, FALSE, '1', 'TICKET ID DEL CLIENTE BRUCE WAYNE ID 1101', CURDATE());
INSERT INTO `vn`.`claimDms`(`claimFk`, `dmsFk`)
VALUES
(1, 7),
(1, 8);
INSERT INTO `vn`.`ticketDms`(`ticketFk`, `dmsFk`) INSERT INTO `vn`.`ticketDms`(`ticketFk`, `dmsFk`)
VALUES VALUES

View File

@ -731,7 +731,7 @@ export default {
claimAction: { claimAction: {
importClaimButton: 'vn-claim-action vn-button[label="Import claim"]', importClaimButton: 'vn-claim-action vn-button[label="Import claim"]',
anyLine: 'vn-claim-action vn-tbody > vn-tr', anyLine: 'vn-claim-action vn-tbody > vn-tr',
firstDeleteLine: 'vn-claim-action vn-tr:nth-child(1) vn-icon-button[icon="delete"]', firstDeleteLine: 'vn-claim-action tr:nth-child(1) vn-icon-button[icon="delete"]',
isPaidWithManaCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.isChargedToMana"]' isPaidWithManaCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.isChargedToMana"]'
}, },
ordersIndex: { ordersIndex: {
@ -1133,7 +1133,7 @@ export default {
entryLatestBuys: { entryLatestBuys: {
firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(1)', firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(1)',
allBuysCheckBox: 'vn-entry-latest-buys thead vn-check', allBuysCheckBox: 'vn-entry-latest-buys thead vn-check',
secondBuyCheckBox: 'vn-entry-latest-buys tbody tr:nth-child(2) vn-check[ng-model="buy.$checked"]', secondBuyCheckBox: 'vn-entry-latest-buys tbody tr:nth-child(2) vn-check[ng-model="buy.checked"]',
editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]', editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]',
fieldAutocomplete: 'vn-autocomplete[ng-model="$ctrl.editedColumn.field"]', fieldAutocomplete: 'vn-autocomplete[ng-model="$ctrl.editedColumn.field"]',
newValueInput: 'vn-textfield[ng-model="$ctrl.editedColumn.newValue"]', newValueInput: 'vn-textfield[ng-model="$ctrl.editedColumn.newValue"]',

View File

@ -111,7 +111,7 @@ describe('Client lock verified data path', () => {
await page.waitToClick(selectors.clientFiscalData.saveButton); await page.waitToClick(selectors.clientFiscalData.saveButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain(`You can't make changes on a client with verified data`); expect(message.text).toContain(`Not enough privileges to edit a client with verified data`);
}); });
}); });
@ -123,19 +123,19 @@ describe('Client lock verified data path', () => {
await page.accessToSection('client.card.fiscalData'); await page.accessToSection('client.card.fiscalData');
}, 20000); }, 20000);
it('should confirm verified data button is enabled for salesAssistant', async() => { it('should confirm verified data button is disabled for salesAssistant', async() => {
const isDisabled = await page.isDisabled(selectors.clientFiscalData.verifiedDataCheckbox); const isDisabled = await page.isDisabled(selectors.clientFiscalData.verifiedDataCheckbox);
expect(isDisabled).toBeFalsy(); expect(isDisabled).toBeTrue();
}); });
it('should now edit the social name', async() => { it('should return error when edit the social name', async() => {
await page.clearInput(selectors.clientFiscalData.socialName); await page.clearInput(selectors.clientFiscalData.socialName);
await page.write(selectors.clientFiscalData.socialName, 'new social name edition'); await page.write(selectors.clientFiscalData.socialName, 'new social name edition');
await page.waitToClick(selectors.clientFiscalData.saveButton); await page.waitToClick(selectors.clientFiscalData.saveButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!'); expect(message.text).toContain(`Not enough privileges to edit a client with verified data`);
}); });
it('should now confirm the social name have been edited once and for all', async() => { it('should now confirm the social name have been edited once and for all', async() => {

View File

@ -0,0 +1 @@
SelectAllRows: Select the {{rows}} row(s)

View File

@ -0,0 +1,3 @@
SelectAllRows: Seleccionar las {{rows}} fila(s)
All: Se han seleccionado
row(s) have been selected.: fila(s).

View File

@ -3,3 +3,22 @@
indeterminate="$ctrl.isIndeterminate" indeterminate="$ctrl.isIndeterminate"
translate-attr="{title: 'Check all'}"> translate-attr="{title: 'Check all'}">
</vn-check> </vn-check>
<vn-icon-button
class="vn-pl-none"
ng-if="$ctrl.checked && $ctrl.checkDummyEnabled"
icon="expand_more"
vn-popover="menu"
ng-click="$ctrl.countRows()">
</vn-icon-button>
<vn-menu vn-id="menu">
<vn-list>
<span translate>All</span>
<span class="bold">
{{$ctrl.rows}}
</span>
<span translate>row(s) have been selected.</span>
<span class="bold link" ng-click="$ctrl.checkDummy()">
{{$ctrl.allRowsText}}
</span>
</vn-list>
</vn-menu>

View File

@ -106,6 +106,9 @@ export default class MultiCheck extends FormInput {
this.toggle(); this.toggle();
this.emit('change', value); this.emit('change', value);
if (!value)
this.checkedDummyCount = null;
} }
/** /**
@ -132,12 +135,43 @@ export default class MultiCheck extends FormInput {
areAllUnchecked() { areAllUnchecked() {
if (!this.model || !this.model.data) return; if (!this.model || !this.model.data) return;
this.checkedDummyCount = null;
const data = this.model.data; const data = this.model.data;
return data.every(item => { return data.every(item => {
return item[this.checkField] === false; return item[this.checkField] === false;
}); });
} }
countRows() {
if (!this.model || !this.model.data) return;
const data = this.model.data;
const modelParams = this.model.userParams;
const params = {
filter: {
modelParams: modelParams,
limit: null
}
};
this.rows = data.length;
this.$http.get(this.model.url, {params})
.then(res => {
this.allRowsCount = res.data.length;
this.allRowsText = this.$t('SelectAllRows', {
rows: this.allRowsCount
});
});
}
checkDummy() {
if (this.checkedDummyCount)
return this.checkedDummyCount = null;
this.checkedDummyCount = this.allRowsCount;
}
/** /**
* Toggles checked property on * Toggles checked property on
* all instances * all instances
@ -158,7 +192,9 @@ ngModule.vnComponent('vnMultiCheck', {
checkField: '@?', checkField: '@?',
checkAll: '=?', checkAll: '=?',
checked: '=?', checked: '=?',
disabled: '<?' disabled: '<?',
checkDummyEnabled: '<?',
checkedDummyCount: '=?'
} }
}); });

View File

@ -4,10 +4,14 @@ import crudModel from 'core/mocks/crud-model';
describe('Component vnMultiCheck', () => { describe('Component vnMultiCheck', () => {
let controller; let controller;
let $element; let $element;
let $httpBackend;
let $httpParamSerializer;
beforeEach(ngModule('vnCore')); beforeEach(ngModule('vnCore'));
beforeEach(inject($componentController => { beforeEach(inject(($componentController, _$httpBackend_, _$httpParamSerializer_) => {
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
$element = angular.element(`<div class="shown"></div>`); $element = angular.element(`<div class="shown"></div>`);
controller = $componentController('vnMultiCheck', {$element: $element}); controller = $componentController('vnMultiCheck', {$element: $element});
controller._model = crudModel; controller._model = crudModel;
@ -26,6 +30,14 @@ describe('Component vnMultiCheck', () => {
expect(controller._checked).toEqual(crudModel); expect(controller._checked).toEqual(crudModel);
expect(controller.toggle).toHaveBeenCalledWith(); expect(controller.toggle).toHaveBeenCalledWith();
}); });
it(`should set checkedDummyCount to null`, () => {
jest.spyOn(controller, 'toggle');
controller.checkedDummyCount = 12;
controller.checked = null;
expect(controller.checkedDummyCount).toBeNull();
});
}); });
describe('toggle()', () => { describe('toggle()', () => {
@ -132,4 +144,50 @@ describe('Component vnMultiCheck', () => {
expect(thirdRow.checked).toBeTruthy(); expect(thirdRow.checked).toBeTruthy();
}); });
}); });
describe('countRows()', () => {
it(`should count visible rows and all rows of model`, () => {
controller.model.url = 'modelUrl/filter';
const data = controller.model.data;
const filter = {
limit: null
};
const serializedParams = $httpParamSerializer({filter});
const response = [
{id: 1, name: 'My item 1'},
{id: 2, name: 'My item 2'},
{id: 3, name: 'My item 3'},
{id: 4, name: 'My item 4'},
{id: 5, name: 'My item 5'},
{id: 6, name: 'My item 6'}
];
controller.countRows();
$httpBackend.expectGET(`modelUrl/filter?${serializedParams}`).respond(response);
$httpBackend.flush();
expect(controller.rows).toEqual(data.length);
expect(controller.allRowsCount).toEqual(response.length);
expect(controller.allRowsCount).toBeGreaterThan(controller.rows);
});
});
describe('checkDummy()', () => {
const allRows = 1234;
it(`should set the checked dummy count to all rows count if there was no count yet`, () => {
controller.checkedDummyCount = null;
controller.allRowsCount = allRows;
controller.checkDummy();
expect(controller.checkedDummyCount).toEqual(controller.allRowsCount);
});
it(`should remove the dummy count if there was an existing one`, () => {
controller.checkedDummyCount = allRows;
controller.checkDummy();
expect(controller.checkedDummyCount).toBeNull();
});
});
}); });

View File

@ -1,5 +1,17 @@
@import "variables";
vn-multi-check { vn-multi-check {
.vn-check { .vn-check {
margin-bottom: 12px margin-bottom: 12px
} }
vn-list{
padding: 50px;
}
vn-menu{
padding: 50px;
}
}
.bold{
font-weight: bold;
} }

View File

@ -26,7 +26,7 @@
.icon-agency-term:before { .icon-agency-term:before {
content: "\e950"; content: "\e950";
} }
.icon-deaulter:before { .icon-defaulter:before {
content: "\e94b"; content: "\e94b";
} }
.icon-100:before { .icon-100:before {

View File

@ -12,7 +12,6 @@
"That payment method requires an IBAN": "That payment method requires an IBAN", "That payment method requires an IBAN": "That payment method requires an IBAN",
"That payment method requires a BIC": "That payment method requires a BIC", "That payment method requires a BIC": "That payment method requires a BIC",
"The default consignee can not be unchecked": "The default consignee can not be unchecked", "The default consignee can not be unchecked": "The default consignee can not be unchecked",
"You can't make changes on a client with verified data": "You can't make changes on a client with verified data",
"Enter an integer different to zero": "Enter an integer different to zero", "Enter an integer different to zero": "Enter an integer different to zero",
"Package cannot be blank": "Package cannot be blank", "Package cannot be blank": "Package cannot be blank",
"The new quantity should be smaller than the old one": "The new quantity should be smaller than the old one", "The new quantity should be smaller than the old one": "The new quantity should be smaller than the old one",
@ -123,5 +122,6 @@
"The type of business must be filled in basic data": "The type of business must be filled in basic data", "The type of business must be filled in basic data": "The type of business must be filled in basic data",
"The worker has hours recorded that day": "The worker has hours recorded that day", "The worker has hours recorded that day": "The worker has hours recorded that day",
"isWithoutNegatives": "isWithoutNegatives", "isWithoutNegatives": "isWithoutNegatives",
"routeFk": "routeFk" "routeFk": "routeFk",
"Not enough privileges to edit a client with verified data": "Not enough privileges to edit a client with verified data"
} }

View File

@ -50,7 +50,7 @@
"You don't have enough privileges to change that field": "No tienes permisos para cambiar ese campo", "You don't have enough privileges to change that field": "No tienes permisos para cambiar ese campo",
"Warehouse cannot be blank": "El almacén no puede quedar en blanco", "Warehouse cannot be blank": "El almacén no puede quedar en blanco",
"Agency cannot be blank": "La agencia no puede quedar en blanco", "Agency cannot be blank": "La agencia no puede quedar en blanco",
"You can't make changes on a client with verified data": "No puedes hacer cambios en un cliente con datos comprobados", "Not enough privileges to edit a client with verified data": "No tienes permisos para hacer cambios en un cliente con datos comprobados",
"This address doesn't exist": "Este consignatario no existe", "This address doesn't exist": "Este consignatario no existe",
"You must delete the claim id %d first": "Antes debes borrar la reclamación %d", "You must delete the claim id %d first": "Antes debes borrar la reclamación %d",
"You don't have enough privileges": "No tienes suficientes permisos", "You don't have enough privileges": "No tienes suficientes permisos",

View File

@ -39,7 +39,8 @@
"multipart/x-zip", "multipart/x-zip",
"image/png", "image/png",
"image/jpeg", "image/jpeg",
"image/jpg" "image/jpg",
"video/mp4"
] ]
}, },
"dmsStorage": { "dmsStorage": {
@ -84,5 +85,18 @@
"application/octet-stream", "application/octet-stream",
"application/pdf" "application/pdf"
] ]
},
"claimStorage": {
"name": "claimStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "31457280",
"allowedContentTypes": [
"image/png",
"image/jpeg",
"image/jpg",
"video/mp4"
]
} }
} }

View File

@ -0,0 +1,49 @@
module.exports = Self => {
Self.remoteMethodCtx('deleteClamedSales', {
description: 'Deletes the claimed sales',
accessType: 'WRITE',
accepts: [{
arg: 'sales',
type: ['object'],
required: true,
description: 'The sales to remove'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteClamedSales`,
verb: 'POST'
}
});
Self.deleteClamedSales = async(ctx, sales, options) => {
const models = Self.app.models;
const myOptions = {};
const tx = await Self.beginTransaction({});
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction)
myOptions.transaction = tx;
try {
const promises = [];
for (let sale of sales) {
const deletedSale = models.ClaimEnd.destroyById(sale.id, myOptions);
promises.push(deletedSale);
}
const deletedSales = await Promise.all(promises);
if (tx) await tx.commit();
return deletedSales;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,69 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethodCtx('filter', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
}, {
arg: 'search',
type: 'string',
description: `If it's and integer searchs by id, otherwise it searchs by client id`,
http: {source: 'query'}
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/filter`,
verb: 'GET'
}
});
Self.filter = async(ctx, filter, options) => {
const conn = Self.dataSource.connector;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const stmts = [];
const stmt = new ParameterizedSQL(
`SELECT *
FROM (
SELECT
ce.id,
ce.claimFk,
s.itemFk,
s.ticketFk,
ce.claimDestinationFk,
t.landed,
s.quantity,
s.concept,
s.price,
s.discount,
s.quantity * s.price * ((100 - s.discount) / 100) total
FROM vn.claimEnd ce
LEFT JOIN vn.sale s ON s.id = ce.saleFk
LEFT JOIN vn.ticket t ON t.id = s.ticketFk
) ce`
);
stmt.merge(conn.makeSuffix(filter));
const itemsIndex = stmts.push(stmt) - 1;
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return itemsIndex === 0 ? result : result[itemsIndex];
};
};

View File

@ -0,0 +1,59 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('downloadFile', {
description: 'Get the claim file',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
},
{
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
},
{
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: `/:id/downloadFile`,
verb: 'GET'
}
});
Self.downloadFile = async function(ctx, id) {
const models = Self.app.models;
const ClaimContainer = models.ClaimContainer;
const dms = await models.Dms.findById(id);
const pathHash = ClaimContainer.getHash(dms.id);
try {
await ClaimContainer.getFile(pathHash, dms.file);
} catch (e) {
if (e.code != 'ENOENT')
throw e;
const error = new UserError(`File doesn't exists`);
error.statusCode = 404;
throw error;
}
const stream = ClaimContainer.downloadStream(pathHash, dms.file);
return [stream, dms.contentType, `filename="${dms.file}"`];
};
};

View File

@ -0,0 +1,13 @@
const app = require('vn-loopback/server/server');
describe('claim downloadFile()', () => {
const dmsId = 7;
it('should return a response for an employee with image content-type', async() => {
const workerId = 1107;
const ctx = {req: {accessToken: {userId: workerId}}};
const result = await app.models.Claim.downloadFile(ctx, dmsId);
expect(result[1]).toEqual('image/jpeg');
});
});

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('claim uploadFile()', () => {
it(`should return an error for a user without enough privileges`, async() => {
const clientId = 1101;
const ticketDmsTypeId = 14;
const ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: ticketDmsTypeId}};
let error;
await app.models.Claim.uploadFile(ctx).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,55 @@
module.exports = Self => {
Self.remoteMethod('updateClaimDestination', {
description: 'Update a claim with privileges',
accessType: 'WRITE',
accepts: [{
arg: 'rows',
type: ['object'],
required: true,
description: `the sales which will be modified the claimDestinationFk`
}, {
arg: 'claimDestinationFk',
type: 'number',
required: true
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/updateClaimDestination`,
verb: 'post'
}
});
Self.updateClaimDestination = async(rows, claimDestinationFk, options) => {
const tx = await Self.beginTransaction({});
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction)
myOptions.transaction = tx;
try {
const models = Self.app.models;
const promises = [];
for (let row of rows) {
const claimEnd = await models.ClaimEnd.findById(row.id, null, myOptions);
const updatedClaimEnd = claimEnd.updateAttribute('claimDestinationFk', claimDestinationFk, myOptions);
promises.push(updatedClaimEnd);
}
const updatedSales = await Promise.all(promises);
if (tx) await tx.commit();
return updatedSales;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -1,6 +1,10 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('uploadFile', { Self.remoteMethodCtx('uploadFile', {
description: 'Upload and attach a document', description: 'Upload and attach a file',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [{
arg: 'id', arg: 'id',
@ -53,22 +57,54 @@ module.exports = Self => {
}); });
Self.uploadFile = async(ctx, id, options) => { Self.uploadFile = async(ctx, id, options) => {
let tx; const tx = await Self.beginTransaction({});
const myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
if (!myOptions.transaction) { if (!myOptions.transaction)
tx = await Self.beginTransaction({});
myOptions.transaction = tx; myOptions.transaction = tx;
}
const models = Self.app.models; const models = Self.app.models;
const promises = []; const promises = [];
const TempContainer = models.TempContainer;
const ClaimContainer = models.ClaimContainer;
const fileOptions = {};
const args = ctx.args;
let srcFile;
try { try {
const uploadedFiles = await models.Dms.uploadFile(ctx, myOptions); const hasWriteRole = await models.DmsType.hasWriteRole(ctx, args.dmsTypeId, myOptions);
uploadedFiles.forEach(dms => { if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
// Upload file to temporary path
const tempContainer = await TempContainer.container('dms');
const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const files = Object.values(uploaded.files).map(file => {
return file[0];
});
const addedDms = [];
for (const uploadedFile of files) {
const newDms = await createDms(ctx, uploadedFile, myOptions);
const pathHash = ClaimContainer.getHash(newDms.id);
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
srcFile = path.join(file.client.root, file.container, file.name);
const claimContainer = await ClaimContainer.container(pathHash);
const dstFile = path.join(claimContainer.client.root, pathHash, newDms.file);
await fs.move(srcFile, dstFile, {
overwrite: true
});
addedDms.push(newDms);
}
addedDms.forEach(dms => {
const newClaimDms = models.ClaimDms.create({ const newClaimDms = models.ClaimDms.create({
claimFk: id, claimFk: id,
dmsFk: dms.id dmsFk: dms.id
@ -83,7 +119,34 @@ module.exports = Self => {
return resolvedPromises; return resolvedPromises;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
if (fs.existsSync(srcFile))
await fs.unlink(srcFile);
throw e; throw e;
} }
}; };
async function createDms(ctx, file, myOptions) {
const models = Self.app.models;
const myUserId = ctx.req.accessToken.userId;
const args = ctx.args;
const newDms = await models.Dms.create({
workerFk: myUserId,
dmsTypeFk: args.dmsTypeId,
companyFk: args.companyId,
warehouseFk: args.warehouseId,
reference: args.reference,
description: args.description,
contentType: file.type,
hasFile: args.hasFile
}, myOptions);
let fileName = file.name;
const extension = models.DmsContainer.getFileExtension(fileName);
fileName = `${newDms.id}.${extension}`;
return newDms.updateAttribute('file', fileName, myOptions);
}
}; };

View File

@ -37,5 +37,8 @@
}, },
"ClaimLog": { "ClaimLog": {
"dataSource": "vn" "dataSource": "vn"
},
"ClaimContainer": {
"dataSource": "claimStorage"
} }
} }

View File

@ -0,0 +1,10 @@
{
"name": "ClaimContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/claim-end/filter')(Self);
require('../methods/claim-end/deleteClamedSales')(Self);
};

View File

@ -7,4 +7,6 @@ module.exports = Self => {
require('../methods/claim/uploadFile')(Self); require('../methods/claim/uploadFile')(Self);
require('../methods/claim/updateClaimAction')(Self); require('../methods/claim/updateClaimAction')(Self);
require('../methods/claim/isEditable')(Self); require('../methods/claim/isEditable')(Self);
require('../methods/claim/updateClaimDestination')(Self);
require('../methods/claim/downloadFile')(Self);
}; };

View File

@ -1,10 +1,8 @@
<vn-crud-model vn-id="model" <vn-crud-model vn-id="model"
url="ClaimEnds" url="ClaimEnds/filter"
filter="$ctrl.filter"
link="{claimFk: $ctrl.$params.id}" link="{claimFk: $ctrl.$params.id}"
data="$ctrl.salesClaimed" data="$ctrl.salesClaimed"
auto-load="true" auto-load="true"
auto-save="true"
on-save="$ctrl.onSave()"> on-save="$ctrl.onSave()">
</vn-crud-model> </vn-crud-model>
<vn-crud-model <vn-crud-model
@ -19,101 +17,124 @@
</vn-label-value> </vn-label-value>
</vn-card> </vn-card>
<vn-card class="vn-pa-lg vn-w-lg"> <vn-card class="vn-pa-lg vn-w-lg">
<section class="header"> <smart-table
<vn-tool-bar class="vn-mb-md"> model="model"
<vn-button options="$ctrl.smartTableOptions"
label="Import claim" expr-builder="$ctrl.exprBuilder(param, value)">
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedStateId" <slot-actions>
vn-http-click="$ctrl.importToNewRefundTicket()" <section class="header">
translate-attr="{title: 'Imports claim details'}"> <vn-tool-bar class="vn-mb-md">
</vn-button> <vn-button
<vn-range label="Import claim"
label="Responsability" disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedStateId"
min-label="Company" vn-http-click="$ctrl.importToNewRefundTicket()"
max-label="Sales/Client" translate-attr="{title: 'Imports claim details'}">
ng-model="$ctrl.claim.responsibility" </vn-button>
max="$ctrl.maxResponsibility" <vn-button
min="1" label="Change destination"
step="1" disabled="$ctrl.checked.length == 0"
on-change="$ctrl.save({responsibility: value})"> ng-click="changeDestination.show()">
</vn-range> </vn-button>
</vn-tool-bar> <vn-range
<vn-check vn-one label="Responsability"
label="Is paid with mana" min-label="Company"
ng-model="$ctrl.claim.isChargedToMana" max-label="Sales/Client"
on-change="$ctrl.save({isChargedToMana: value})"> ng-model="$ctrl.claim.responsibility"
</vn-check> max="$ctrl.maxResponsibility"
</section> min="1"
<vn-data-viewer model="model"> step="1"
<vn-table model="model"> on-change="$ctrl.save({responsibility: value})">
<vn-thead> </vn-range>
<vn-tr> </vn-tool-bar>
<vn-th number>Id</vn-th> <vn-check class="right"
<vn-th number>Ticket</vn-th> vn-one
<vn-th>Destination</vn-th> label="Is paid with mana"
<vn-th expand>Landed</vn-th> ng-model="$ctrl.claim.isChargedToMana"
<vn-th number>Quantity</vn-th> on-change="$ctrl.save({isChargedToMana: value})">
<vn-th>Description</vn-th> </vn-check>
<vn-th number>Price</vn-th> </section>
<vn-th number>Disc.</vn-th> </slot-actions>
<vn-th number>Total</vn-th> <slot-table>
</vn-tr> <table model="model">
</vn-thead> <thead>
<vn-tbody> <tr>
<vn-tr <th shrink>
<vn-multi-check
model="model"
check-field="$checked">
</vn-multi-check>
</th>
<th number field="itemFk">Id</th>
<th number field="ticketFk">Ticket</th>
<th field="claimDestinationFk">Destination</th>
<th expand field="landed">Landed</th>
<th number field="quantity">Quantity</th>
<th field="concept">Description</th>
<th number field="price">Price</th>
<th number field="discount">Disc.</th>
<th number field="total">Total</th>
</tr>
</thead>
<tbody>
<tr
ng-repeat="saleClaimed in $ctrl.salesClaimed" ng-repeat="saleClaimed in $ctrl.salesClaimed"
vn-repeat-last on-last="$ctrl.focusLastInput()"> vn-repeat-last on-last="$ctrl.focusLastInput()">
<vn-td number> <td>
<span <vn-check
ng-click="itemDescriptor.show($event, saleClaimed.sale.itemFk)" ng-model="saleClaimed.$checked"
vn-click-stop>
</vn-check>
</td>
<td number>
<vn-span
ng-click="itemDescriptor.show($event, saleClaimed.itemFk)"
class="link"> class="link">
{{::saleClaimed.sale.itemFk | zeroFill:6}} {{::saleClaimed.itemFk | zeroFill:6}}
</span> </vn-span>
</vn-td> </td>
<vn-td number> <td number>
<span <vn-span
class="link" class="link"
ng-click="ticketDescriptor.show($event, saleClaimed.sale.ticketFk)"> ng-click="ticketDescriptor.show($event, saleClaimed.ticketFk)">
{{::saleClaimed.sale.ticketFk}} {{::saleClaimed.ticketFk}}
</span> </vn-span>
</vn-td> </td>
<vn-td expand> <td expand>
<vn-autocomplete vn-one id="claimDestinationFk" <vn-autocomplete vn-one id="claimDestinationFk"
ng-model="saleClaimed.claimDestinationFk" ng-model="saleClaimed.claimDestinationFk"
data="claimDestinations" data="claimDestinations"
on-change="$ctrl.updateDestination(saleClaimed, value)"
fields="['id','description']" fields="['id','description']"
value-field="id" value-field="id"
show-field="description"> show-field="description">
</vn-autocomplete> </vn-autocomplete>
</vn-td> </td>
<vn-td expand>{{::saleClaimed.sale.ticket.landed | date: 'dd/MM/yyyy'}}</vn-td> <td expand>{{::saleClaimed.landed | date: 'dd/MM/yyyy'}}</td>
<vn-td number>{{::saleClaimed.sale.quantity}}</vn-td> <td number>{{::saleClaimed.quantity}}</td>
<vn-td expand>{{::saleClaimed.sale.concept}}</vn-td> <td expand>{{::saleClaimed.concept}}</td>
<vn-td number>{{::saleClaimed.sale.price | currency: 'EUR':2}}</vn-td> <td number>{{::saleClaimed.price | currency: 'EUR':2}}</td>
<vn-td number>{{::saleClaimed.sale.discount}} %</vn-td> <td number>{{::saleClaimed.discount}} %</td>
<vn-td number> <td number>{{saleClaimed.total | currency: 'EUR':2}}</td>
{{saleClaimed.sale.quantity * saleClaimed.sale.price * <td shrink>
((100 - saleClaimed.sale.discount) / 100) | currency: 'EUR':2}}
</vn-td>
<vn-td shrink>
<vn-icon-button <vn-icon-button
vn-tooltip="Remove line" vn-tooltip="Remove line"
icon="delete" icon="delete"
ng-click="model.remove($index)" ng-click="$ctrl.removeSales(saleClaimed)"
tabindex="-1"> tabindex="-1">
</vn-icon-button> </vn-icon-button>
</vn-td> </td>
</vn-tr> </tr>
</vn-tbody> </tbody>
</vn-table> </table>
</vn-data-viewer> </slot-table>
<vn-button-bar> </smart-table>
<vn-button <button-bar class="vn-pa-md">
label="Regularize" <vn-button
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedStateId" label="Regularize"
vn-http-click="$ctrl.regularize()"> disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedStateId"
</vn-button> vn-http-click="$ctrl.regularize()">
</vn-button-bar> </vn-button>
</button-bar>
</vn-card> </vn-card>
<vn-item-descriptor-popover <vn-item-descriptor-popover
vn-id="item-descriptor" vn-id="item-descriptor"
@ -128,3 +149,28 @@
message="Do you want to insert greuges?" message="Do you want to insert greuges?"
on-accept="$ctrl.onUpdateGreugeAccept()"> on-accept="$ctrl.onUpdateGreugeAccept()">
</vn-confirm> </vn-confirm>
<!-- Dialog of change destionation -->
<vn-dialog
vn-id="changeDestination"
on-accept="$ctrl.onResponse()">
<tpl-body>
<section class="SMSDialog">
<h5 class="vn-py-sm">{{$ctrl.$t('Change destination to all selected rows', {total: $ctrl.checked.length})}}</h5>
<vn-horizontal>
<vn-autocomplete vn-one id="claimDestinationFk"
ng-model="$ctrl.newDestination"
data="claimDestinations"
fields="['id','description']"
value-field="id"
show-field="description"
vn-focus>
</vn-autocomplete>
</vn-horizontal>
</section>
</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

@ -5,6 +5,7 @@ import './style.scss';
export default class Controller extends Section { export default class Controller extends Section {
constructor($element, $) { constructor($element, $) {
super($element, $); super($element, $);
this.newDestination;
this.filter = { this.filter = {
include: [ include: [
{relation: 'sale', {relation: 'sale',
@ -21,6 +22,81 @@ export default class Controller extends Section {
}; };
this.getResolvedState(); this.getResolvedState();
this.maxResponsibility = 5; this.maxResponsibility = 5;
this.smartTableOptions = {
activeButtons: {
search: true
},
columns: [
{
field: 'claimDestinationFk',
autocomplete: {
url: 'ClaimDestinations',
showField: 'description',
valueField: 'id'
}
},
{
field: 'landed',
searchable: false
}
]
};
}
exprBuilder(param, value) {
switch (param) {
case 'itemFk':
case 'ticketFk':
case 'claimDestinationFk':
case 'quantity':
case 'price':
case 'discount':
case 'total':
return {[param]: value};
case 'concept':
return {[param]: {like: `%${value}%`}};
case 'landed':
return {[param]: {between: this.dateRange(value)}};
}
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
}
get checked() {
const salesClaimed = this.$.model.data || [];
const checkedSalesClaimed = [];
for (let saleClaimed of salesClaimed) {
if (saleClaimed.$checked)
checkedSalesClaimed.push(saleClaimed);
}
return checkedSalesClaimed;
}
updateDestination(saleClaimed, claimDestinationFk) {
const data = {rows: [saleClaimed], claimDestinationFk: claimDestinationFk};
this.$http.post(`Claims/updateClaimDestination`, data).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
}).catch(e => {
this.$.model.refresh();
throw e;
});
}
removeSales(saleClaimed) {
const params = {sales: [saleClaimed]};
this.$http.post(`ClaimEnds/deleteClamedSales`, params).then(() => {
this.$.model.refresh();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
} }
getResolvedState() { getResolvedState() {
@ -54,8 +130,8 @@ export default class Controller extends Section {
calculateTotals() { calculateTotals() {
this.claimedTotal = 0; this.claimedTotal = 0;
this.salesClaimed.forEach(sale => { this.salesClaimed.forEach(sale => {
const price = sale.sale.quantity * sale.sale.price; const price = sale.quantity * sale.price;
const discount = (sale.sale.discount * (sale.sale.quantity * sale.sale.price)) / 100; const discount = (sale.discount * (sale.quantity * sale.price)) / 100;
this.claimedTotal += price - discount; this.claimedTotal += price - discount;
}); });
} }
@ -125,6 +201,24 @@ export default class Controller extends Section {
onSave() { onSave() {
this.vnApp.showSuccess(this.$t('Data saved!')); this.vnApp.showSuccess(this.$t('Data saved!'));
} }
onResponse() {
const rowsToEdit = [];
for (let row of this.checked)
rowsToEdit.push({id: row.id});
const data = {
rows: rowsToEdit,
claimDestinationFk: this.newDestination
};
const query = `Claims/updateClaimDestination`;
this.$http.post(query, data)
.then(() => {
this.$.model.refresh();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
} }
ngModule.vnComponent('vnClaimAction', { ngModule.vnComponent('vnClaimAction', {

View File

@ -43,9 +43,9 @@ describe('claim', () => {
describe('calculateTotals()', () => { describe('calculateTotals()', () => {
it('should calculate the total price of the items claimed', () => { it('should calculate the total price of the items claimed', () => {
controller.salesClaimed = [ controller.salesClaimed = [
{sale: {quantity: 5, price: 2, discount: 0}}, {quantity: 5, price: 2, discount: 0},
{sale: {quantity: 10, price: 2, discount: 0}}, {quantity: 10, price: 2, discount: 0},
{sale: {quantity: 10, price: 2, discount: 0}} {quantity: 10, price: 2, discount: 0}
]; ];
controller.calculateTotals(); controller.calculateTotals();
@ -151,5 +151,17 @@ describe('claim', () => {
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Greuge added'); expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Greuge added');
}); });
}); });
describe('onResponse()', () => {
it('should perform a post query and show a success message', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expect('POST', `Claims/updateClaimDestination`).respond({});
controller.onResponse();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
}); });
}); });

View File

@ -8,3 +8,6 @@ Do you want to insert greuges?: Desea insertar greuges?
Insert greuges on client card: Insertar greuges en la ficha del cliente Insert greuges on client card: Insertar greuges en la ficha del cliente
Greuge added: Greuge añadido Greuge added: Greuge añadido
ClaimGreugeDescription: Reclamación id {{claimId}} ClaimGreugeDescription: Reclamación id {{claimId}}
Change destination: Cambiar destino
Change destination to all selected rows: Cambiar destino a {{total}} fila(s) seleccionada(s)
Add observation to all selected clients: Añadir observación a {{total}} cliente(s) seleccionado(s)

View File

@ -6,7 +6,7 @@ vn-claim-action {
align-content: center; align-content: center;
vn-tool-bar { vn-tool-bar {
flex: 1 flex: none
} }
.vn-check { .vn-check {
@ -39,4 +39,8 @@ vn-claim-action {
max-height: 350px; max-height: 350px;
} }
} }
.right {
margin-left: 370px;
}
} }

View File

@ -1,6 +1,7 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
auto-load="true" auto-load="true"
filter="::$ctrl.filter"
url="ClaimDms" url="ClaimDms"
link="{claimFk: $ctrl.$params.id}" link="{claimFk: $ctrl.$params.id}"
limit="20" limit="20"
@ -14,8 +15,13 @@
<section class="photo" ng-repeat="photo in $ctrl.photos"> <section class="photo" ng-repeat="photo in $ctrl.photos">
<section class="image vn-shadow" on-error-src <section class="image vn-shadow" on-error-src
ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}" ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}"
zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}"> zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}"
ng-if="photo.dms.contentType != 'video/mp4'">
</section> </section>
<video id="videobcg" muted="muted" controls ng-if="photo.dms.contentType == 'video/mp4'"
class="video">
<source src="{{$ctrl.getImagePath(photo.dmsFk)}}" type="video/mp4">
</video>
<section class="actions"> <section class="actions">
<vn-button <vn-button
class="round" class="round"
@ -35,7 +41,7 @@
</vn-confirm> </vn-confirm>
<vn-float-button <vn-float-button
icon="add" icon="add"
vn-tooltip="Select photo" vn-tooltip="Select file"
vn-bind="+" vn-bind="+"
ng-click="$ctrl.openUploadDialog()" ng-click="$ctrl.openUploadDialog()"
fixed-bottom-right> fixed-bottom-right>

View File

@ -6,6 +6,13 @@ class Controller extends Section {
constructor($element, $, vnFile) { constructor($element, $, vnFile) {
super($element, $); super($element, $);
this.vnFile = vnFile; this.vnFile = vnFile;
this.filter = {
include: [
{
relation: 'dms'
}
]
};
} }
deleteDms(index) { deleteDms(index) {
@ -13,7 +20,7 @@ class Controller extends Section {
return this.$http.post(`ClaimDms/${dmsFk}/removeFile`) return this.$http.post(`ClaimDms/${dmsFk}/removeFile`)
.then(() => { .then(() => {
this.$.model.remove(index); this.$.model.remove(index);
this.vnApp.showSuccess(this.$t('Photo deleted')); this.vnApp.showSuccess(this.$t('File deleted'));
}); });
} }
@ -81,13 +88,13 @@ class Controller extends Section {
data: this.dms.files data: this.dms.files
}; };
this.$http(options).then(() => { this.$http(options).then(() => {
this.vnApp.showSuccess(this.$t('Photo uploaded!')); this.vnApp.showSuccess(this.$t('File uploaded!'));
this.$.model.refresh(); this.$.model.refresh();
}); });
} }
getImagePath(dmsId) { getImagePath(dmsId) {
return this.vnFile.getPath(`/api/dms/${dmsId}/downloadFile`); return this.vnFile.getPath(`/api/Claims/${dmsId}/downloadFile`);
} }
} }

View File

@ -1,5 +1,5 @@
Are you sure you want to continue?: ¿Seguro que quieres continuar? Are you sure you want to continue?: ¿Seguro que quieres continuar?
Drag & Drop photos here...: Arrastra y suelta fotos aquí... Drag & Drop photos here...: Arrastra y suelta fotos aquí...
Photo deleted: Foto eliminada File deleted: Archivo eliminado
Photo uploaded!: Foto subida! File uploaded!: Archivo subido!
Select photo: Seleccionar foto Select file: Seleccionar fichero

View File

@ -29,4 +29,19 @@ vn-claim-photos {
height: 288px; height: 288px;
} }
} }
.video {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),
0 3px 1px -2px rgba(0,0,0,.2),
0 1px 5px 0 rgba(0,0,0,.12);
border: 2px solid transparent;
}
.video:hover {
border: 2px solid $color-primary
}
} }

View File

@ -1,6 +1,22 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Client addressesPropagateRe', () => { describe('Client addressesPropagateRe', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should propagate the isEqualizated on both addresses of Mr Wayne and set hasToInvoiceByAddress to false', async() => { it('should propagate the isEqualizated on both addresses of Mr Wayne and set hasToInvoiceByAddress to false', async() => {
const tx = await models.Client.beginTransaction({}); const tx = await models.Client.beginTransaction({});

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Address createAddress', () => { describe('Address createAddress', () => {
const clientFk = 1101; const clientFk = 1101;
@ -6,6 +7,21 @@ describe('Address createAddress', () => {
const incotermsFk = 'FAS'; const incotermsFk = 'FAS';
const customAgentOneId = 1; const customAgentOneId = 1;
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should throw a non uee member error if no incoterms is defined', async() => { it('should throw a non uee member error if no incoterms is defined', async() => {
const tx = await models.Client.beginTransaction({}); const tx = await models.Client.beginTransaction({});

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Client Create', () => { describe('Client Create', () => {
const newAccount = { const newAccount = {
@ -12,6 +13,21 @@ describe('Client Create', () => {
businessTypeFk: 'florist' businessTypeFk: 'florist'
}; };
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it(`should not find Deadpool as he's not created yet`, async() => { it(`should not find Deadpool as he's not created yet`, async() => {
const tx = await models.Client.beginTransaction({}); const tx = await models.Client.beginTransaction({});

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Address updateAddress', () => { describe('Address updateAddress', () => {
const clientId = 1101; const clientId = 1101;
@ -13,6 +14,21 @@ describe('Address updateAddress', () => {
} }
}; };
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should throw the non uee member error if no incoterms is defined', async() => { it('should throw the non uee member error if no incoterms is defined', async() => {
const tx = await models.Client.beginTransaction({}); const tx = await models.Client.beginTransaction({});
@ -93,6 +109,7 @@ describe('Address updateAddress', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
ctx.req.accessToken.userId = employeeId;
ctx.args = { ctx.args = {
isLogifloraAllowed: true isLogifloraAllowed: true
}; };

View File

@ -1,10 +1,25 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Client updateFiscalData', () => { describe('Client updateFiscalData', () => {
const clientId = 1101; const clientId = 1101;
const employeeId = 1; const employeeId = 1;
const salesAssistantId = 21; const salesAssistantId = 21;
const administrativeId = 5; const administrativeId = 5;
const activeCtx = {
accessToken: {userId: employeeId},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should return an error if the user is not salesAssistant and the isTaxDataChecked value is true', async() => { it('should return an error if the user is not salesAssistant and the isTaxDataChecked value is true', async() => {
const tx = await models.Client.beginTransaction({}); const tx = await models.Client.beginTransaction({});
@ -24,7 +39,7 @@ describe('Client updateFiscalData', () => {
error = e; error = e;
} }
expect(error.message).toEqual(`You can't make changes on a client with verified data`); expect(error.message).toEqual(`Not enough privileges to edit a client with verified data`);
}); });
it('should return an error if the salesAssistant did not fill the sage data before checking verified data', async() => { it('should return an error if the salesAssistant did not fill the sage data before checking verified data', async() => {

View File

@ -125,11 +125,11 @@ module.exports = Self => {
} }
try { try {
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions); const isAdministrative = await models.Account.hasRole(userId, 'administrative', myOptions);
const client = await models.Client.findById(clientId, null, myOptions); const client = await models.Client.findById(clientId, null, myOptions);
if (!isSalesAssistant && client.isTaxDataChecked) if (!isAdministrative && client.isTaxDataChecked)
throw new UserError(`You can't make changes on a client with verified data`); throw new UserError(`Not enough privileges to edit a client with verified data`);
// Sage data validation // Sage data validation
const taxDataChecked = args.isTaxDataChecked; const taxDataChecked = args.isTaxDataChecked;

View File

@ -46,10 +46,6 @@ module.exports = Self => {
message: 'TIN must be unique' message: 'TIN must be unique'
}); });
Self.validatesUniquenessOf('socialName', {
message: 'The company name must be unique'
});
Self.validatesFormatOf('email', { Self.validatesFormatOf('email', {
message: 'Invalid email', message: 'Invalid email',
allowNull: true, allowNull: true,
@ -63,17 +59,37 @@ module.exports = Self => {
min: 3, max: 10 min: 3, max: 10
}); });
Self.validateAsync('socialName', socialNameIsUnique, {
message: 'The company name must be unique'
});
async function socialNameIsUnique(err, done) {
const filter = {
where: {
and: [
{socialName: this.socialName},
{isActive: true},
{id: {neq: this.id}}
]
}
};
const client = await Self.app.models.Client.findOne(filter);
if (client)
err();
done();
}
Self.validateAsync('iban', ibanNeedsValidation, { Self.validateAsync('iban', ibanNeedsValidation, {
message: 'The IBAN does not have the correct format' message: 'The IBAN does not have the correct format'
}); });
async function ibanNeedsValidation(err, done) { async function ibanNeedsValidation(err, done) {
let filter = { const filter = {
fields: ['code'], fields: ['code'],
where: {id: this.countryFk} where: {id: this.countryFk}
}; };
let country = await Self.app.models.Country.findOne(filter); const country = await Self.app.models.Country.findOne(filter);
let code = country ? country.code.toLowerCase() : null; const code = country ? country.code.toLowerCase() : null;
if (code != 'es') if (code != 'es')
return done(); return done();
@ -90,12 +106,12 @@ module.exports = Self => {
if (!this.isTaxDataChecked) if (!this.isTaxDataChecked)
return done(); return done();
let filter = { const filter = {
fields: ['code'], fields: ['code'],
where: {id: this.countryFk} where: {id: this.countryFk}
}; };
let country = await Self.app.models.Country.findOne(filter); const country = await Self.app.models.Country.findOne(filter);
let code = country ? country.code.toLowerCase() : null; const code = country ? country.code.toLowerCase() : null;
if (!this.fi || !validateTin(this.fi, code)) if (!this.fi || !validateTin(this.fi, code))
err(); err();
@ -118,8 +134,8 @@ module.exports = Self => {
function cannotHaveET(err) { function cannotHaveET(err) {
if (!this.fi) return; if (!this.fi) return;
let tin = this.fi.toUpperCase(); const tin = this.fi.toUpperCase();
let cannotHaveET = /^[A-B]/.test(tin); const cannotHaveET = /^[A-B]/.test(tin);
if (cannotHaveET && this.isEqualizated) if (cannotHaveET && this.isEqualizated)
err(); err();
@ -208,6 +224,34 @@ module.exports = Self => {
throw new UserError(`The type of business must be filled in basic data`); throw new UserError(`The type of business must be filled in basic data`);
}); });
Self.observe('before save', async ctx => {
const changes = ctx.data || ctx.instance;
const orgData = ctx.currentInstance;
const models = Self.app.models;
const loopBackContext = LoopBackContext.getCurrentContext();
const userId = loopBackContext.active.accessToken.userId;
const isAdministrative = await models.Account.hasRole(userId, 'administrative', ctx.options);
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', ctx.options);
const hasChanges = orgData && changes;
const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked);
const isTaxDataCheckedChanged = hasChanges && orgData.isTaxDataChecked != isTaxDataChecked;
const sageTaxType = hasChanges && (changes.sageTaxTypeFk || orgData.sageTaxTypeFk);
const sageTaxTypeChanged = hasChanges && orgData.sageTaxTypeFk != sageTaxType;
const sageTransactionType = hasChanges && (changes.sageTransactionTypeFk || orgData.sageTransactionTypeFk);
const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType;
const cantEditVerifiedData = isTaxDataCheckedChanged && !isAdministrative;
const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !isSalesAssistant;
if (cantEditVerifiedData || cantChangeSageData)
throw new UserError(`You don't have enough privileges`);
});
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
const changes = ctx.data || ctx.instance; const changes = ctx.data || ctx.instance;
const orgData = ctx.currentInstance; const orgData = ctx.currentInstance;
@ -221,10 +265,10 @@ module.exports = Self => {
const socialNameChanged = hasChanges const socialNameChanged = hasChanges
&& orgData.socialName != socialName; && orgData.socialName != socialName;
const dataCheckedChanged = hasChanges const isTaxDataCheckedChanged = hasChanges
&& orgData.isTaxDataChecked != isTaxDataChecked; && orgData.isTaxDataChecked != isTaxDataChecked;
if ((socialNameChanged || dataCheckedChanged) && !isAlpha(socialName)) if ((socialNameChanged || isTaxDataCheckedChanged) && !isAlpha(socialName))
throw new UserError(`The social name has an invalid format`); throw new UserError(`The social name has an invalid format`);
if (changes.salesPerson === null) { if (changes.salesPerson === null) {

View File

@ -1,20 +1,36 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('loopback model address', () => { describe('loopback model address', () => {
let createdAddressId; let createdAddressId;
const clientId = 1101; const clientId = 1101;
afterAll(async() => { const activeCtx = {
let client = await app.models.Client.findById(clientId); accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
await app.models.Address.destroyById(createdAddressId); beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
afterAll(async() => {
const client = await models.Client.findById(clientId);
await models.Address.destroyById(createdAddressId);
await client.updateAttribute('isEqualizated', false); await client.updateAttribute('isEqualizated', false);
}); });
describe('observe()', () => { describe('observe()', () => {
it('should throw an error when deactivating a consignee if its the default address', async() => { it('should throw an error when deactivating a consignee if its the default address', async() => {
let error; let error;
let address = await app.models.Address.findById(1); const address = await models.Address.findById(1);
await address.updateAttribute('isActive', false) await address.updateAttribute('isActive', false)
.catch(e => { .catch(e => {
@ -27,13 +43,13 @@ describe('loopback model address', () => {
}); });
it('should set isEqualizated to true of a given Client to trigger any new address to have it', async() => { it('should set isEqualizated to true of a given Client to trigger any new address to have it', async() => {
let client = await app.models.Client.findById(clientId); const client = await models.Client.findById(clientId);
expect(client.isEqualizated).toBeFalsy(); expect(client.isEqualizated).toBeFalsy();
await client.updateAttribute('isEqualizated', true); await client.updateAttribute('isEqualizated', true);
let newAddress = await app.models.Address.create({ const newAddress = await models.Address.create({
clientFk: clientId, clientFk: clientId,
agencyModeFk: 5, agencyModeFk: 5,
city: 'here', city: 'here',

View File

@ -105,7 +105,7 @@
<div ng-transclude="btnFive"> <div ng-transclude="btnFive">
<vn-quick-link <vn-quick-link
ng-if="$ctrl.client.supplier.nif" ng-if="$ctrl.client.supplier.nif"
tooltip="Go to client" tooltip="Go to supplier"
state="['supplier.card.summary', {id: $ctrl.client.supplier.id}]" state="['supplier.card.summary', {id: $ctrl.client.supplier.id}]"
icon="icon-supplier"> icon="icon-supplier">
</vn-quick-link> </vn-quick-link>

View File

@ -3,5 +3,6 @@ View consumer report: Ver informe de consumo
From date: Fecha desde From date: Fecha desde
To date: Fecha hasta To date: Fecha hasta
Go to user: Ir al usuario Go to user: Ir al usuario
Go to supplier: Ir al proveedor
Client invoices list: Listado de facturas del cliente Client invoices list: Listado de facturas del cliente
Pay method: Forma de pago Pay method: Forma de pago

View File

@ -33,7 +33,7 @@
label="Social name" label="Social name"
ng-model="$ctrl.client.socialName" ng-model="$ctrl.client.socialName"
rule rule
info="You can use letters and spaces" info="Only letters, numbers and spaces can be used"
required="true"> required="true">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
@ -182,7 +182,7 @@
vn-one vn-one
label="Verified data" label="Verified data"
ng-model="$ctrl.client.isTaxDataChecked" ng-model="$ctrl.client.isTaxDataChecked"
vn-acl="salesAssistant"> vn-acl="administrative">
</vn-check> </vn-check>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>

View File

@ -3,7 +3,7 @@ You changed the equalization tax: Has cambiado el recargo de equivalencia
Do you want to spread the change?: ¿Deseas propagar el cambio a sus consignatarios? Do you want to spread the change?: ¿Deseas propagar el cambio a sus consignatarios?
Frozen: Congelado Frozen: Congelado
In order to invoice, this field is not consulted, but the consignee's ET. When modifying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not.: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automáticamente el cambio a todos los consignatarios, en caso contrario preguntará al usuario si quiere o no propagar. In order to invoice, this field is not consulted, but the consignee's ET. When modifying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not.: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automáticamente el cambio a todos los consignatarios, en caso contrario preguntará al usuario si quiere o no propagar.
You can use letters and spaces: Se pueden utilizar letras y espacios Only letters, numbers and spaces can be used: Sólo se pueden usar letras, numeros y espacios
Found a client with this data: Se ha encontrado un cliente con estos datos Found a client with this data: Se ha encontrado un cliente con estos datos
Found a client with this phone or email: El cliente con id <a href="#!/client/{{clientId}}/summary" target="_blank">{{clientId}}</a> ya tiene este teléfono o email. <br/> ¿Quieres continuar? Found a client with this phone or email: El cliente con id <a href="#!/client/{{clientId}}/summary" target="_blank">{{clientId}}</a> ya tiene este teléfono o email. <br/> ¿Quieres continuar?
Sage tax type: Tipo de impuesto Sage Sage tax type: Tipo de impuesto Sage

View File

@ -81,7 +81,7 @@ export default class Controller extends Section {
clientIds: clientIds clientIds: clientIds
}, this.campaign); }, this.campaign);
this.$http.post('notify/consumption', params) this.$http.post('schedule/consumption', params)
.then(() => this.$.filters.hide()) .then(() => this.$.filters.hide())
.then(() => this.vnApp.showSuccess(this.$t('Notifications sent!'))); .then(() => this.vnApp.showSuccess(this.$t('Notifications sent!')));
} }

View File

@ -74,7 +74,7 @@ describe('Client notification', () => {
clientIds: [1101, 1102] clientIds: [1101, 1102]
}, controller.campaign); }, controller.campaign);
$httpBackend.expect('POST', `notify/consumption`, params).respond(200, params); $httpBackend.expect('POST', `schedule/consumption`, params).respond(200, params);
controller.onSendClientConsumption(); controller.onSendClientConsumption();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -1,27 +1,32 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('editLatestBuys', { Self.remoteMethodCtx('editLatestBuys', {
description: 'Updates a column for one or more buys', description: 'Updates a column for one or more buys',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [{
arg: 'field', arg: 'field',
type: 'String', type: 'string',
required: true, required: true,
description: `the column to edit` description: `the column to edit`
}, },
{ {
arg: 'newValue', arg: 'newValue',
type: 'Any', type: 'any',
required: true, required: true,
description: `The new value to save` description: `The new value to save`
}, },
{ {
arg: 'lines', arg: 'lines',
type: ['Object'], type: ['object'],
required: true, required: true,
description: `the buys which will be modified` description: `the buys which will be modified`
},
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
}], }],
returns: { returns: {
type: 'Object', type: 'object',
root: true root: true
}, },
http: { http: {
@ -30,7 +35,7 @@ module.exports = Self => {
} }
}); });
Self.editLatestBuys = async(field, newValue, lines, options) => { Self.editLatestBuys = async(ctx, field, newValue, lines, filter, options) => {
let tx; let tx;
const myOptions = {}; const myOptions = {};
@ -64,17 +69,19 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const model = models[modelName]; const model = models[modelName];
try { try {
const promises = []; const promises = [];
const value = {};
value[field] = newValue;
if (filter) {
ctx.args.filter = {where: filter, limit: null};
lines = await models.Buy.latestBuysFilter(ctx, null, myOptions);
}
const targets = lines.map(line => { const targets = lines.map(line => {
return line[identifier]; return line[identifier];
}); });
const value = {};
value[field] = newValue;
for (let target of targets) for (let target of targets)
promises.push(model.upsertWithWhere({id: target}, value, myOptions)); promises.push(model.upsertWithWhere({id: target}, value, myOptions));

View File

@ -86,7 +86,7 @@ module.exports = Self => {
} }
], ],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -98,6 +98,9 @@ module.exports = Self => {
Self.latestBuysFilter = async(ctx, filter, options) => { Self.latestBuysFilter = async(ctx, filter, options) => {
const myOptions = {}; const myOptions = {};
if (filter && filter.modelParams)
ctx.args = filter.modelParams;
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -6,7 +6,7 @@ describe('Buy editLatestsBuys()', () => {
const options = {transaction: tx}; const options = {transaction: tx};
try { try {
let ctx = { const ctx = {
args: { args: {
search: 'Ranged weapon longbow 2m' search: 'Ranged weapon longbow 2m'
} }
@ -18,7 +18,35 @@ describe('Buy editLatestsBuys()', () => {
const newValue = 99; const newValue = 99;
const lines = [{itemFk: original.itemFk, id: original.id}]; const lines = [{itemFk: original.itemFk, id: original.id}];
await models.Buy.editLatestBuys(field, newValue, lines, options); await models.Buy.editLatestBuys(ctx, field, newValue, lines, null, options);
const [result] = await models.Buy.latestBuysFilter(ctx, null, options);
expect(result[field]).toEqual(newValue);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should change the value of a given column for filter', async() => {
const tx = await models.Buy.beginTransaction({});
const options = {transaction: tx};
try {
const filter = {typeFk: 1};
const ctx = {
args: {
filter: filter
}
};
const field = 'size';
const newValue = 88;
await models.Buy.editLatestBuys(ctx, field, newValue, null, filter, options);
const [result] = await models.Buy.latestBuysFilter(ctx, null, options); const [result] = await models.Buy.latestBuysFilter(ctx, null, options);

View File

@ -29,9 +29,11 @@
<tr> <tr>
<th shrink> <th shrink>
<vn-multi-check <vn-multi-check
checked="$ctrl.checkAll"
model="model" model="model"
check-field="$checked"> checked="$ctrl.checkAll"
check-field="checked"
check-dummy-enabled="true"
checked-dummy-count="$ctrl.checkedDummyCount">
</vn-multi-check> </vn-multi-check>
</th> </th>
<th translate>Picture</th> <th translate>Picture</th>
@ -126,7 +128,7 @@
}"> }">
<td> <td>
<vn-check <vn-check
ng-model="buy.$checked" ng-model="buy.checked"
vn-click-stop> vn-click-stop>
</vn-check> </vn-check>
</td> </td>

View File

@ -6,7 +6,7 @@ export default class Controller extends Section {
constructor($element, $) { constructor($element, $) {
super($element, $); super($element, $);
this.editedColumn; this.editedColumn;
this.$checkAll = false; this.checkAll = false;
this.smartTableOptions = { this.smartTableOptions = {
activeButtons: { activeButtons: {
@ -91,7 +91,7 @@ export default class Controller extends Section {
const buys = this.$.model.data || []; const buys = this.$.model.data || [];
const checkedBuys = []; const checkedBuys = [];
for (let buy of buys) { for (let buy of buys) {
if (buy.$checked) if (buy.checked)
checkedBuys.push(buy); checkedBuys.push(buy);
} }
@ -142,6 +142,9 @@ export default class Controller extends Section {
} }
get totalChecked() { get totalChecked() {
if (this.checkedDummyCount)
return this.checkedDummyCount;
return this.checked.length; return this.checked.length;
} }
@ -156,6 +159,9 @@ export default class Controller extends Section {
lines: rowsToEdit lines: rowsToEdit
}; };
if (this.checkedDummyCount && this.checkedDummyCount > 0)
data.filter = this.$.model.userParams;
return this.$http.post('Buys/editLatestBuys', data) return this.$http.post('Buys/editLatestBuys', data)
.then(() => { .then(() => {
this.uncheck(); this.uncheck();

View File

@ -31,10 +31,10 @@ describe('Entry', () => {
describe('get checked', () => { describe('get checked', () => {
it(`should return a set of checked lines`, () => { it(`should return a set of checked lines`, () => {
controller.$.model.data = [ controller.$.model.data = [
{$checked: true, id: 1}, {checked: true, id: 1},
{$checked: true, id: 2}, {checked: true, id: 2},
{$checked: true, id: 3}, {checked: true, id: 3},
{$checked: false, id: 4}, {checked: false, id: 4},
]; ];
let result = controller.checked; let result = controller.checked;

View File

@ -1,6 +1,6 @@
<vn-crud-model <vn-crud-model
vn-id="buysModel" vn-id="buysModel"
url="Entries/{{$ctrl.$params.id}}/getBuys" url="Entries/{{$ctrl.entry.id}}/getBuys"
limit="5" limit="5"
data="buys" data="buys"
auto-load="true"> auto-load="true">

View File

@ -4,6 +4,9 @@ import Summary from 'salix/components/summary';
class Controller extends Summary { class Controller extends Summary {
get entry() { get entry() {
if (!this._entry)
return this.$params;
return this._entry; return this._entry;
} }

View File

@ -144,6 +144,7 @@ module.exports = Self => {
ii.isBooked, ii.isBooked,
ii.supplierRef, ii.supplierRef,
ii.docFk AS dmsFk, ii.docFk AS dmsFk,
dm.file,
ii.supplierFk, ii.supplierFk,
ii.expenceFkDeductible deductibleExpenseFk, ii.expenceFkDeductible deductibleExpenseFk,
s.name AS supplierName, s.name AS supplierName,
@ -156,7 +157,8 @@ module.exports = Self => {
LEFT JOIN duaInvoiceIn dii ON dii.invoiceInFk = ii.id LEFT JOIN duaInvoiceIn dii ON dii.invoiceInFk = ii.id
LEFT JOIN dua d ON d.id = dii.duaFk LEFT JOIN dua d ON d.id = dii.duaFk
LEFT JOIN awb ON awb.id = d.awbFk LEFT JOIN awb ON awb.id = d.awbFk
LEFT JOIN company co ON co.id = ii.companyFk` LEFT JOIN company co ON co.id = ii.companyFk
LEFT JOIN dms dm ON dm.id = ii.docFk`
); );
const sqlWhere = conn.makeWhere(filter.where); const sqlWhere = conn.makeWhere(filter.where);

View File

@ -30,7 +30,7 @@ module.exports = Self => {
SUM(iidd.amount) totalDueDay SUM(iidd.amount) totalDueDay
FROM vn.invoiceIn ii FROM vn.invoiceIn ii
LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase, LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase,
SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) totalVat CAST(SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) AS DECIMAL(10,2)) totalVat
FROM vn.invoiceInTax iit FROM vn.invoiceInTax iit
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
WHERE iit.invoiceInFk = ?) iit ON TRUE WHERE iit.invoiceInFk = ?) iit ON TRUE

View File

@ -13,13 +13,14 @@
vn-id="supplier" vn-id="supplier"
url="Suppliers" url="Suppliers"
label="Supplier" label="Supplier"
search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}" search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}, {nif: {like: '%'+ $search +'%'}}]}"
fields="['nif']"
show-field="name" show-field="name"
value-field="id" value-field="id"
ng-model="$ctrl.invoiceIn.supplierFk" ng-model="$ctrl.invoiceIn.supplierFk"
order="id" order="id"
vn-focus> vn-focus>
<tpl-item>{{id}}: {{name}}</tpl-item> <tpl-item>{{id}}: {{nif}}: {{name}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
<vn-textfield <vn-textfield
vn-one vn-one
@ -30,14 +31,6 @@
label="Issued" label="Issued"
ng-model="$ctrl.invoiceIn.issued"> ng-model="$ctrl.invoiceIn.issued">
</vn-date-picker> </vn-date-picker>
<vn-autocomplete
vn-one
label="Currency"
ng-model="$ctrl.invoiceIn.currencyFk"
url="Currencies"
show-field="code"
value-field="id">
</vn-autocomplete>
<vn-autocomplete <vn-autocomplete
vn-one vn-one
label="Company" label="Company"

View File

@ -13,7 +13,7 @@
<vn-th field="supplierRef">Supplier ref.</vn-th> <vn-th field="supplierRef">Supplier ref.</vn-th>
<vn-th field="serialNumber">Serial number</vn-th> <vn-th field="serialNumber">Serial number</vn-th>
<vn-th field="serial">Serial</vn-th> <vn-th field="serial">Serial</vn-th>
<vn-th field="account">Account</vn-th> <vn-th field="dmsFk">File</vn-th>
<vn-th field="issued" expand>Issued</vn-th> <vn-th field="issued" expand>Issued</vn-th>
<vn-th field="isBooked" center>Is booked</vn-th> <vn-th field="isBooked" center>Is booked</vn-th>
<vn-th field="awbCode" vn-tooltip="Air Waybill">AWB</vn-th> <vn-th field="awbCode" vn-tooltip="Air Waybill">AWB</vn-th>
@ -27,7 +27,7 @@
class="clickable vn-tr search-result" class="clickable vn-tr search-result"
ui-sref="invoiceIn.card.summary({id: {{::invoiceIn.id}}})"> ui-sref="invoiceIn.card.summary({id: {{::invoiceIn.id}}})">
<vn-td>{{::invoiceIn.id}}</vn-td> <vn-td>{{::invoiceIn.id}}</vn-td>
<vn-td> <vn-td expand>
<span <span
class="link" class="link"
vn-click-stop="supplierDescriptor.show($event, invoiceIn.supplierFk)"> vn-click-stop="supplierDescriptor.show($event, invoiceIn.supplierFk)">
@ -37,7 +37,12 @@
<vn-td>{{::invoiceIn.supplierRef | dashIfEmpty}}</vn-td> <vn-td>{{::invoiceIn.supplierRef | dashIfEmpty}}</vn-td>
<vn-td>{{::invoiceIn.serialNumber}}</vn-td> <vn-td>{{::invoiceIn.serialNumber}}</vn-td>
<vn-td>{{::invoiceIn.serial}}</vn-td> <vn-td>{{::invoiceIn.serial}}</vn-td>
<vn-td>{{::invoiceIn.account}}</vn-td> <vn-td>
<span title="{{'Download file' | translate}}" class="link"
ng-click="$ctrl.downloadFile(invoiceIn.dmsFk)">
{{::invoiceIn.file}}
</span>
</vn-td>
<vn-td expand>{{::invoiceIn.issued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td> <vn-td expand>{{::invoiceIn.issued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
<vn-td center> <vn-td center>
<vn-check disabled="true" <vn-check disabled="true"

View File

@ -2,6 +2,11 @@ import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
export default class Controller extends Section { export default class Controller extends Section {
constructor($element, $, vnFile) {
super($element, $, vnFile);
this.vnFile = vnFile;
}
exprBuilder(param, value) { exprBuilder(param, value) {
switch (param) { switch (param) {
case 'issued': case 'issued':
@ -39,8 +44,14 @@ export default class Controller extends Section {
this.selectedInvoiceIn = invoiceIn; this.selectedInvoiceIn = invoiceIn;
this.$.summary.show(); this.$.summary.show();
} }
downloadFile(dmsId) {
this.vnFile.download(`api/dms/${dmsId}/downloadFile`);
}
} }
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.vnComponent('vnInvoiceInIndex', { ngModule.vnComponent('vnInvoiceInIndex', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller controller: Controller

View File

@ -1,5 +1,3 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('globalInvoicing', { Self.remoteMethodCtx('globalInvoicing', {
description: 'Make a global invoice', description: 'Make a global invoice',
@ -140,6 +138,9 @@ module.exports = Self => {
if (newInvoice.id) { if (newInvoice.id) {
await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions); await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions);
query = `INSERT IGNORE INTO invoiceOut_queue(invoiceFk) VALUES(?)`;
await Self.rawSql(query, [newInvoice.id], myOptions);
invoicesIds.push(newInvoice.id); invoicesIds.push(newInvoice.id);
} }
} catch (e) { } catch (e) {
@ -160,10 +161,6 @@ module.exports = Self => {
throw e; throw e;
} }
// Print invoices PDF
for (let invoiceId of invoicesIds)
await Self.createPdf(ctx, invoiceId);
return invoicesIds; return invoicesIds;
}; };
@ -212,7 +209,13 @@ module.exports = Self => {
const query = `SELECT const query = `SELECT
c.id, c.id,
SUM(IFNULL(s.quantity * s.price * (100-s.discount)/100, 0) + IFNULL(ts.quantity * ts.price,0)) AS sumAmount, SUM(IFNULL
(
s.quantity *
s.price * (100-s.discount)/100,
0)
+ IFNULL(ts.quantity * ts.price,0)
) AS sumAmount,
c.hasToInvoiceByAddress, c.hasToInvoiceByAddress,
c.email, c.email,
c.isToBeMailed, c.isToBeMailed,

View File

@ -93,6 +93,9 @@ describe('InvoiceOut createManualInvoice()', () => {
it('should throw an error for a non-invoiceable client', async() => { it('should throw an error for a non-invoiceable client', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true))); spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const tx = await models.InvoiceOut.beginTransaction({}); const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};

View File

@ -19,7 +19,7 @@
ng-if="$ctrl.isInvoicing"> ng-if="$ctrl.isInvoicing">
<vn-horizontal> <vn-horizontal>
<vn-icon vn-none icon="warning"></vn-icon> <vn-icon vn-none icon="warning"></vn-icon>
<span vn-none translate>Invoicing in progress...</span> <span vn-none translate>Adding invoices to queue...</span>
</vn-horizontal> </vn-horizontal>
</div> </div>
<vn-horizontal> <vn-horizontal>
@ -66,5 +66,5 @@
</tpl-body> </tpl-body>
<tpl-buttons> <tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/> <input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate vn-focus>Make invoice</button> <button response="accept" translate vn-focus>Invoice</button>
</tpl-buttons> </tpl-buttons>

View File

@ -1,7 +1,7 @@
Create global invoice: Crear factura global Create global invoice: Crear factura global
Some fields are required: Algunos campos son obligatorios Some fields are required: Algunos campos son obligatorios
Max date: Fecha límite Max date: Fecha límite
Invoicing in progress...: Facturación en progreso... Adding invoices to queue...: Añadiendo facturas a la cola...
Invoice date: Fecha de factura Invoice date: Fecha de factura
From client: Desde el cliente From client: Desde el cliente
To client: Hasta el cliente To client: Hasta el cliente

View File

@ -50,7 +50,7 @@
<th field="salesPersonFk"> <th field="salesPersonFk">
<span translate>Salesperson</span> <span translate>Salesperson</span>
</th> </th>
<th field="shipped" shrink-date> <th field="shippedDate" shrink-date>
<span translate>Date</span> <span translate>Date</span>
</th> </th>
<th field="theoreticalHour"> <th field="theoreticalHour">
@ -153,8 +153,8 @@
</span> </span>
</td> </td>
<td> <td>
<span class="chip {{::$ctrl.compareDate(ticket.shipped)}}"> <span class="chip {{::$ctrl.compareDate(ticket.shippedDate)}}">
{{::ticket.shipped | date: 'dd/MM/yyyy'}} {{::ticket.shippedDate | date: 'dd/MM/yyyy'}}
</span> </span>
</td> </td>
<td>{{::ticket.zoneLanding | date: 'HH:mm'}}</td> <td>{{::ticket.zoneLanding | date: 'HH:mm'}}</td>

View File

@ -52,7 +52,7 @@ export default class Controller extends Section {
} }
}, },
{ {
field: 'shipped', field: 'shippedDate',
searchable: false searchable: false
}, },
{ {
@ -136,7 +136,7 @@ export default class Controller extends Section {
return {'z.hour': value}; return {'z.hour': value};
case 'practicalHour': case 'practicalHour':
return {'zed.etc': value}; return {'zed.etc': value};
case 'shipped': case 'shippedDate':
return {'t.shipped': { return {'t.shipped': {
between: this.dateRange(value)} between: this.dateRange(value)}
}; };

View File

@ -33,7 +33,7 @@
{{::order.name | dashIfEmpty}} {{::order.name | dashIfEmpty}}
</span> </span>
</vn-td> </vn-td>
<vn-td expand> <vn-td>
<span <span
vn-click-stop="clientDescriptor.show($event, order.clientFk)" vn-click-stop="clientDescriptor.show($event, order.clientFk)"
class="link"> class="link">
@ -46,7 +46,7 @@
disabled="true"> disabled="true">
</vn-check> </vn-check>
</vn-td> </vn-td>
<vn-td center>{{::order.created | date: 'dd/MM/yyyy HH:mm'}}</vn-td> <vn-td shrink-datetime>{{::order.created | date: 'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td shrink-date> <vn-td shrink-date>
<span class="chip {{$ctrl.compareDate(order.landed)}}"> <span class="chip {{$ctrl.compareDate(order.landed)}}">
{{::order.landed | date:'dd/MM/yyyy'}} {{::order.landed | date:'dd/MM/yyyy'}}

View File

@ -100,12 +100,7 @@ module.exports = Self => {
}, },
] ]
}; };
let supplier = await Self.app.models.Supplier.findOne(filter);
const farmerCode = 2; return Self.app.models.Supplier.findOne(filter);
if (supplier.sageWithholdingFk == farmerCode)
supplier.isFarmer = true;
return supplier;
}; };
}; };

View File

@ -25,12 +25,4 @@ describe('Supplier getSummary()', () => {
expect(payMethod.name).toEqual('PayMethod one'); expect(payMethod.name).toEqual('PayMethod one');
}); });
it(`should get if supplier is farmer by sageWithholdingFk`, async() => {
const supplier = await app.models.Supplier.findById(2);
const supplierSummary = await app.models.Supplier.getSummary(2);
expect(supplier.isFarmer).toBeUndefined();
expect(supplierSummary.isFarmer).toEqual(true);
});
}); });

View File

@ -78,7 +78,7 @@
<vn-float-button <vn-float-button
vn-bind="+" vn-bind="+"
fixed-bottom-right fixed-bottom-right
vn-tooltip="New row" vn-tooltip="Add row"
ui-sref="supplier.card.agencyTerm.create" ui-sref="supplier.card.agencyTerm.create"
icon="add" icon="add"
label="Add"> label="Add">

View File

@ -38,7 +38,7 @@
vn-focus vn-focus
label="Social name" label="Social name"
ng-model="$ctrl.supplier.name" ng-model="$ctrl.supplier.name"
info="You can use letters and spaces" info="Only letters, numbers and spaces can be used"
required="true" required="true"
rule> rule>
</vn-textfield> </vn-textfield>

View File

@ -83,11 +83,6 @@
label="Account" label="Account"
value="{{::$ctrl.summary.account}}"> value="{{::$ctrl.summary.account}}">
</vn-label-value> </vn-label-value>
<vn-check
label="Is Farmer"
ng-model="$ctrl.summary.isFarmer"
disabled="true">
</vn-check>
</vn-one> </vn-one>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>

View File

@ -20,7 +20,7 @@ class Controller extends Section {
set sales(value) { set sales(value) {
this._sales = value; this._sales = value;
if (value) this.applyVolumes(); if (value && value.length) this.applyVolumes();
} }
applyVolumes() { applyVolumes() {

View File

@ -73,15 +73,14 @@ class Controller extends Section {
for (let event of $events) for (let event of $events)
zoneIds.push(event.zoneFk); zoneIds.push(event.zoneFk);
this.$.zoneEvents.show($event.target);
const params = { const params = {
zoneIds: zoneIds, zoneIds: zoneIds,
date: day date: day
}; };
this.$http.post(`Zones/getZoneClosing`, params) this.$http.post(`Zones/getZoneClosing`, params)
.then(res => this.zoneClosing = res.data); .then(res => this.zoneClosing = res.data)
.then(() => this.$.zoneEvents.show($event.target));
} }
preview(zone) { preview(zone) {

View File

@ -16,7 +16,7 @@ module.exports = [
cb: require('./closure') cb: require('./closure')
}, },
{ {
url: '/api/notify', url: '/api/schedule',
cb: require('./notify') cb: require('./schedule')
} }
]; ];

View File

@ -2,5 +2,6 @@ const express = require('express');
const router = new express.Router(); const router = new express.Router();
router.post('/consumption', require('./consumption')); router.post('/consumption', require('./consumption'));
router.post('/invoice', require('./invoice'));
module.exports = router; module.exports = router;

View File

@ -0,0 +1,114 @@
const db = require('vn-print/core/database');
const Email = require('vn-print/core/email');
const Report = require('vn-print/core/report');
const storage = require('vn-print/core/storage');
module.exports = async function(request, response, next) {
try {
response.status(200).json({
message: 'Success'
});
const invoices = await db.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 invoiceOut_queue 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 connection;
let invoiceId;
for (const invoiceOut of invoices) {
try {
invoiceId = invoiceOut.id;
connection = await db.getConnection();
connection.query('START TRANSACTION');
const args = Object.assign({
invoiceId: invoiceOut.id,
recipientId: invoiceOut.clientFk,
recipient: invoiceOut.recipient,
replyTo: invoiceOut.salesPersonEmail
}, response.locals);
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
});
connection.query('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id]);
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
sql = `UPDATE invoiceOut_queue
SET status = "printed",
printed = NOW()
WHERE invoiceFk = ?`;
connection.query(sql, [invoiceOut.id]);
connection.query('COMMIT');
} catch (error) {
connection.query('ROLLBACK');
connection.release();
sql = `UPDATE invoiceOut_queue
SET status = ?
WHERE invoiceFk = ?`;
await db.rawSql(sql, [error.message, invoiceId]);
}
}
} catch (error) {
next(error);
}
};

View File

@ -7,6 +7,10 @@ const fs = require('fs-extra');
module.exports = { module.exports = {
name: 'delivery-note', name: 'delivery-note',
created() {
if (!this.type)
this.type = 'deliveryNote';
},
async serverPrefetch() { async serverPrefetch() {
this.client = await this.fetchClient(this.ticketId); this.client = await this.fetchClient(this.ticketId);
this.ticket = await this.fetchTicket(this.ticketId); this.ticket = await this.fetchTicket(this.ticketId);
@ -129,7 +133,7 @@ module.exports = {
}, },
type: { type: {
type: String, type: String,
required: true required: false
} }
} }
}; };

BIN
storage/dms/8f1/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

BIN
storage/dms/c9f/8.mp4 Normal file

Binary file not shown.