Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2967-zone_deliverydays_keep_search_on_url
This commit is contained in:
commit
0c71b915ca
|
@ -3,7 +3,6 @@
|
||||||
SCHEMAS=(
|
SCHEMAS=(
|
||||||
account
|
account
|
||||||
bs
|
bs
|
||||||
bi
|
|
||||||
cache
|
cache
|
||||||
edi
|
edi
|
||||||
hedera
|
hedera
|
||||||
|
|
|
@ -182,5 +182,7 @@
|
||||||
"Client assignment has changed": "He cambiado el comercial ~*\"<{{previousWorkerName}}>\"*~ por *\"<{{currentWorkerName}}>\"* del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
|
"Client assignment has changed": "He cambiado el comercial ~*\"<{{previousWorkerName}}>\"*~ por *\"<{{currentWorkerName}}>\"* del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
|
||||||
"None": "Ninguno",
|
"None": "Ninguno",
|
||||||
"The contract was not active during the selected date": "El contrato no estaba activo durante la fecha seleccionada",
|
"The contract was not active during the selected date": "El contrato no estaba activo durante la fecha seleccionada",
|
||||||
"This document already exists on this ticket": "Este documento ya existe en el ticket"
|
"This document already exists on this ticket": "Este documento ya existe en el ticket",
|
||||||
|
"Some of the selected tickets are not billable": "Algunos de los tickets seleccionados no son facturables",
|
||||||
|
"You can't invoice tickets from multiple clients": "No puedes facturar tickets de multiples clientes"
|
||||||
}
|
}
|
|
@ -18,13 +18,8 @@
|
||||||
"acquireTimeout": 20000
|
"acquireTimeout": 20000
|
||||||
},
|
},
|
||||||
"osticket": {
|
"osticket": {
|
||||||
"connector": "vn-mysql",
|
"connector": "memory",
|
||||||
"database": "vn",
|
"timezone": "local"
|
||||||
"debug": false,
|
|
||||||
"host": "localhost",
|
|
||||||
"port": "3306",
|
|
||||||
"username": "root",
|
|
||||||
"password": "root"
|
|
||||||
},
|
},
|
||||||
"tempStorage": {
|
"tempStorage": {
|
||||||
"name": "tempStorage",
|
"name": "tempStorage",
|
||||||
|
|
|
@ -5,7 +5,7 @@ module.exports = function(Self) {
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
arg: 'id',
|
arg: 'id',
|
||||||
type: 'string',
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Client id',
|
description: 'Client id',
|
||||||
http: {source: 'path'}
|
http: {source: 'path'}
|
||||||
|
@ -22,8 +22,18 @@ module.exports = function(Self) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.canBeInvoiced = async id => {
|
Self.canBeInvoiced = async(id, options) => {
|
||||||
let client = await Self.app.models.Client.findById(id, {fields: ['id', 'isTaxDataChecked', 'hasToInvoice']});
|
const models = Self.app.models;
|
||||||
|
|
||||||
|
let myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
const client = await models.Client.findById(id, {
|
||||||
|
fields: ['id', 'isTaxDataChecked', 'hasToInvoice']
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
if (client.isTaxDataChecked && client.hasToInvoice)
|
if (client.isTaxDataChecked && client.hasToInvoice)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
const app = require('vn-loopback/server/server');
|
||||||
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
|
describe('client canBeInvoiced()', () => {
|
||||||
|
const userId = 19;
|
||||||
|
const clientId = 1101;
|
||||||
|
const activeCtx = {
|
||||||
|
accessToken: {userId: userId}
|
||||||
|
};
|
||||||
|
const models = app.models;
|
||||||
|
|
||||||
|
beforeAll(async done => {
|
||||||
|
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||||
|
active: activeCtx
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy for a client without the data checked', async() => {
|
||||||
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const client = await models.Client.findById(clientId, null, options);
|
||||||
|
await client.updateAttribute('isTaxDataChecked', false, options);
|
||||||
|
|
||||||
|
const canBeInvoiced = await models.Client.canBeInvoiced(clientId, options);
|
||||||
|
|
||||||
|
expect(canBeInvoiced).toEqual(false);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy for a client with invoicing disabled', async() => {
|
||||||
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const client = await models.Client.findById(clientId, null, options);
|
||||||
|
await client.updateAttribute('hasToInvoice', false, options);
|
||||||
|
|
||||||
|
const canBeInvoiced = await models.Client.canBeInvoiced(clientId, options);
|
||||||
|
|
||||||
|
expect(canBeInvoiced).toEqual(false);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return truthy for an invoiceable client', async() => {
|
||||||
|
const canBeInvoiced = await models.Client.canBeInvoiced(clientId);
|
||||||
|
|
||||||
|
expect(canBeInvoiced).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
|
@ -60,7 +60,8 @@
|
||||||
<vn-horizontal>
|
<vn-horizontal>
|
||||||
<vn-input-number
|
<vn-input-number
|
||||||
ng-model="$ctrl.deliveredAmount"
|
ng-model="$ctrl.deliveredAmount"
|
||||||
label="Delivered amount">
|
label="Delivered amount"
|
||||||
|
step="0.01">
|
||||||
</vn-input-number>
|
</vn-input-number>
|
||||||
<vn-input-number
|
<vn-input-number
|
||||||
disabled="true"
|
disabled="true"
|
||||||
|
|
|
@ -10,73 +10,73 @@ module.exports = Self => {
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
arg: 'filter',
|
arg: 'filter',
|
||||||
type: 'Object',
|
type: 'object',
|
||||||
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
|
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'search',
|
arg: 'search',
|
||||||
type: 'String',
|
type: 'string',
|
||||||
description: 'Searchs the invoiceOut by id',
|
description: 'Searchs the invoiceOut by id',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'clientFk',
|
arg: 'clientFk',
|
||||||
type: 'Integer',
|
type: 'integer',
|
||||||
description: 'The client id',
|
description: 'The client id',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'fi',
|
arg: 'fi',
|
||||||
type: 'String',
|
type: 'string',
|
||||||
description: 'The client fiscal id',
|
description: 'The client fiscal id',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'hasPdf',
|
arg: 'hasPdf',
|
||||||
type: 'Boolean',
|
type: 'boolean',
|
||||||
description: 'Whether the the invoiceOut has PDF or not',
|
description: 'Whether the the invoiceOut has PDF or not',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'amount',
|
arg: 'amount',
|
||||||
type: 'Number',
|
type: 'number',
|
||||||
description: 'The amount filter',
|
description: 'The amount filter',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'min',
|
arg: 'min',
|
||||||
type: 'Number',
|
type: 'number',
|
||||||
description: 'The minimun amount flter',
|
description: 'The minimun amount flter',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'max',
|
arg: 'max',
|
||||||
type: 'Number',
|
type: 'number',
|
||||||
description: 'The maximun amount flter',
|
description: 'The maximun amount flter',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'issued',
|
arg: 'issued',
|
||||||
type: 'Date',
|
type: 'date',
|
||||||
description: 'The issued date filter',
|
description: 'The issued date filter',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'created',
|
arg: 'created',
|
||||||
type: 'Date',
|
type: 'date',
|
||||||
description: 'The created date filter',
|
description: 'The created date filter',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arg: 'dued',
|
arg: 'dued',
|
||||||
type: 'Date',
|
type: 'date',
|
||||||
description: 'The due date filter',
|
description: 'The due date filter',
|
||||||
http: {source: 'query'}
|
http: {source: 'query'}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
returns: {
|
returns: {
|
||||||
type: ['Object'],
|
type: ['object'],
|
||||||
root: true
|
root: true
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
|
|
|
@ -10,48 +10,57 @@ module.exports = Self => {
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
arg: 'filter',
|
arg: 'filter',
|
||||||
type: 'Object',
|
type: 'object',
|
||||||
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
|
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'tags',
|
arg: 'tags',
|
||||||
type: ['Object'],
|
type: ['object'],
|
||||||
description: 'List of tags to filter with',
|
description: 'List of tags to filter with',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'search',
|
arg: 'search',
|
||||||
type: 'String',
|
type: 'string',
|
||||||
description: `If it's and integer searchs by id, otherwise it searchs by name`,
|
description: `If it's and integer searchs by id, otherwise it searchs by name`,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'id',
|
arg: 'id',
|
||||||
type: 'Integer',
|
type: 'integer',
|
||||||
description: 'Item id',
|
description: 'Item id',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'categoryFk',
|
arg: 'categoryFk',
|
||||||
type: 'Integer',
|
type: 'integer',
|
||||||
description: 'Category id',
|
description: 'Category id',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'typeFk',
|
arg: 'typeFk',
|
||||||
type: 'Integer',
|
type: 'integer',
|
||||||
description: 'Type id',
|
description: 'Type id',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'isActive',
|
arg: 'isActive',
|
||||||
type: 'Boolean',
|
type: 'boolean',
|
||||||
description: 'Whether the the item is or not active',
|
description: 'Whether the the item is or not active',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'salesPersonFk',
|
arg: 'salesPersonFk',
|
||||||
type: 'Integer',
|
type: 'integer',
|
||||||
description: 'The buyer of the item',
|
description: 'The buyer of the item',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'description',
|
arg: 'description',
|
||||||
type: 'String',
|
type: 'string',
|
||||||
description: 'The item description',
|
description: 'The item description',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
arg: 'stemMultiplier',
|
arg: 'stemMultiplier',
|
||||||
type: 'Integer',
|
type: 'integer',
|
||||||
description: 'The item multiplier',
|
description: 'The item multiplier',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
returns: {
|
returns: {
|
||||||
type: ['Object'],
|
type: ['object'],
|
||||||
root: true
|
root: true
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
|
@ -60,23 +69,28 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.filter = async(ctx, filter) => {
|
Self.filter = async(ctx, filter, options) => {
|
||||||
let conn = Self.dataSource.connector;
|
const conn = Self.dataSource.connector;
|
||||||
|
let myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
let codeWhere;
|
let codeWhere;
|
||||||
|
|
||||||
if (ctx.args.search) {
|
if (ctx.args.search) {
|
||||||
let items = await Self.app.models.ItemBarcode.find({
|
const items = await Self.app.models.ItemBarcode.find({
|
||||||
where: {code: ctx.args.search},
|
where: {code: ctx.args.search},
|
||||||
fields: ['itemFk']
|
fields: ['itemFk']
|
||||||
});
|
}, myOptions);
|
||||||
let itemIds = [];
|
const itemIds = [];
|
||||||
for (const item of items)
|
for (const item of items)
|
||||||
itemIds.push(item.itemFk);
|
itemIds.push(item.itemFk);
|
||||||
|
|
||||||
codeWhere = {'i.id': {inq: itemIds}};
|
codeWhere = {'i.id': {inq: itemIds}};
|
||||||
}
|
}
|
||||||
|
|
||||||
let where = buildFilter(ctx.args, (param, value) => {
|
const where = buildFilter(ctx.args, (param, value) => {
|
||||||
switch (param) {
|
switch (param) {
|
||||||
case 'search':
|
case 'search':
|
||||||
return /^\d+$/.test(value)
|
return /^\d+$/.test(value)
|
||||||
|
@ -90,8 +104,8 @@ module.exports = Self => {
|
||||||
return {'i.stemMultiplier': value};
|
return {'i.stemMultiplier': value};
|
||||||
case 'typeFk':
|
case 'typeFk':
|
||||||
return {'i.typeFk': value};
|
return {'i.typeFk': value};
|
||||||
case 'category':
|
case 'categoryFk':
|
||||||
return {'ic.name': value};
|
return {'ic.id': value};
|
||||||
case 'salesPersonFk':
|
case 'salesPersonFk':
|
||||||
return {'it.workerFk': value};
|
return {'it.workerFk': value};
|
||||||
case 'origin':
|
case 'origin':
|
||||||
|
@ -104,7 +118,7 @@ module.exports = Self => {
|
||||||
});
|
});
|
||||||
filter = mergeFilters(filter, {where});
|
filter = mergeFilters(filter, {where});
|
||||||
|
|
||||||
let stmts = [];
|
const stmts = [];
|
||||||
let stmt;
|
let stmt;
|
||||||
|
|
||||||
stmt = new ParameterizedSQL(
|
stmt = new ParameterizedSQL(
|
||||||
|
@ -173,9 +187,10 @@ module.exports = Self => {
|
||||||
stmt.merge(conn.makeWhere(filter.where));
|
stmt.merge(conn.makeWhere(filter.where));
|
||||||
stmt.merge(conn.makePagination(filter));
|
stmt.merge(conn.makePagination(filter));
|
||||||
|
|
||||||
let itemsIndex = stmts.push(stmt) - 1;
|
const itemsIndex = stmts.push(stmt) - 1;
|
||||||
let sql = ParameterizedSQL.join(stmts, ';');
|
const sql = ParameterizedSQL.join(stmts, ';');
|
||||||
let result = await conn.executeStmt(sql);
|
const result = await conn.executeStmt(sql, myOptions);
|
||||||
|
|
||||||
return itemsIndex === 0 ? result : result[itemsIndex];
|
return itemsIndex === 0 ? result : result[itemsIndex];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,29 +2,62 @@ const app = require('vn-loopback/server/server');
|
||||||
|
|
||||||
describe('item filter()', () => {
|
describe('item filter()', () => {
|
||||||
it('should return 1 result filtering by id', async() => {
|
it('should return 1 result filtering by id', async() => {
|
||||||
let filter = {};
|
const tx = await app.models.Item.beginTransaction({});
|
||||||
let result = await app.models.Item.filter({args: {filter: filter, search: 1}});
|
const options = {transaction: tx};
|
||||||
|
|
||||||
expect(result.length).toEqual(1);
|
try {
|
||||||
expect(result[0].id).toEqual(1);
|
const filter = {};
|
||||||
|
const ctx = {args: {filter: filter, search: 1}};
|
||||||
|
const result = await app.models.Item.filter(ctx, filter, options);
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].id).toEqual(1);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 1 result filtering by barcode', async() => {
|
it('should return 1 result filtering by barcode', async() => {
|
||||||
let filter = {};
|
const tx = await app.models.Item.beginTransaction({});
|
||||||
let result = await app.models.Item.filter({args: {filter: filter, search: 4444444444}});
|
const options = {transaction: tx};
|
||||||
|
|
||||||
expect(result.length).toEqual(1);
|
try {
|
||||||
expect(result[0].id).toEqual(2);
|
const filter = {};
|
||||||
|
const ctx = {args: {filter: filter, search: 4444444444}};
|
||||||
|
const result = await app.models.Item.filter(ctx, filter, options);
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].id).toEqual(2);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 2 results using filter and tags', async() => {
|
it('should return 2 results using filter and tags', async() => {
|
||||||
let filter = {
|
const tx = await app.models.Item.beginTransaction({});
|
||||||
order: 'isActive ASC, name',
|
const options = {transaction: tx};
|
||||||
limit: 8
|
|
||||||
};
|
|
||||||
let tags = [{value: 'medical box', tagFk: 58}];
|
|
||||||
let result = await app.models.Item.filter({args: {filter: filter, typeFk: 5, tags: tags}});
|
|
||||||
|
|
||||||
expect(result.length).toEqual(2);
|
try {
|
||||||
|
const filter = {
|
||||||
|
order: 'isActive ASC, name',
|
||||||
|
limit: 8
|
||||||
|
};
|
||||||
|
const tags = [{value: 'medical box', tagFk: 58}];
|
||||||
|
const ctx = {args: {filter: filter, typeFk: 5, tags: tags}};
|
||||||
|
const result = await app.models.Item.filter(ctx, filter, options);
|
||||||
|
|
||||||
|
expect(result.length).toEqual(2);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,11 +4,10 @@ module.exports = function(Self) {
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
arg: 'id',
|
arg: 'ticketsIds',
|
||||||
type: 'number',
|
description: 'The tickets id',
|
||||||
required: true,
|
type: ['number'],
|
||||||
description: 'The ticket id',
|
required: true
|
||||||
http: {source: 'path'}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
returns: {
|
returns: {
|
||||||
|
@ -17,26 +16,44 @@ module.exports = function(Self) {
|
||||||
root: true
|
root: true
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
path: `/:id/canBeInvoiced`,
|
path: `/canBeInvoiced`,
|
||||||
verb: 'get'
|
verb: 'get'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.canBeInvoiced = async id => {
|
Self.canBeInvoiced = async(ticketsIds, options) => {
|
||||||
let ticket = await Self.app.models.Ticket.findById(id, {
|
let myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
const tickets = await Self.find({
|
||||||
|
where: {
|
||||||
|
id: {inq: ticketsIds}
|
||||||
|
},
|
||||||
fields: ['id', 'refFk', 'shipped', 'totalWithVat']
|
fields: ['id', 'refFk', 'shipped', 'totalWithVat']
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT vn.hasSomeNegativeBase(t.id) AS hasSomeNegativeBase
|
||||||
|
FROM ticket t
|
||||||
|
WHERE id IN(?)`;
|
||||||
|
const ticketBases = await Self.rawSql(query, [ticketsIds], myOptions);
|
||||||
|
const hasSomeNegativeBase = ticketBases.some(
|
||||||
|
ticketBases => ticketBases.hasSomeNegativeBase
|
||||||
|
);
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
const invalidTickets = tickets.some(ticket => {
|
||||||
|
const shipped = new Date(ticket.shipped);
|
||||||
|
const shippingInFuture = shipped.getTime() > today.getTime();
|
||||||
|
const isInvoiced = ticket.refFk;
|
||||||
|
const priceZero = ticket.totalWithVat == 0;
|
||||||
|
|
||||||
|
return isInvoiced || priceZero || shippingInFuture;
|
||||||
});
|
});
|
||||||
|
|
||||||
let query = `SELECT vn.hasSomeNegativeBase(?) AS hasSomeNegativeBase`;
|
return !(invalidTickets || hasSomeNegativeBase);
|
||||||
let [result] = await Self.rawSql(query, [id]);
|
|
||||||
let hasSomeNegativeBase = result.hasSomeNegativeBase;
|
|
||||||
|
|
||||||
let today = new Date();
|
|
||||||
let shipped = new Date(ticket.shipped);
|
|
||||||
|
|
||||||
if (ticket.refFk || ticket.totalWithVat == 0 || shipped.getTime() > today.getTime() || hasSomeNegativeBase)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,15 +2,14 @@ const UserError = require('vn-loopback/util/user-error');
|
||||||
|
|
||||||
module.exports = function(Self) {
|
module.exports = function(Self) {
|
||||||
Self.remoteMethodCtx('makeInvoice', {
|
Self.remoteMethodCtx('makeInvoice', {
|
||||||
description: 'Make out an invoice from a ticket id',
|
description: 'Make out an invoice from one or more tickets',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
arg: 'id',
|
arg: 'ticketsIds',
|
||||||
type: 'string',
|
description: 'The tickets id',
|
||||||
required: true,
|
type: ['number'],
|
||||||
description: 'Ticket id',
|
required: true
|
||||||
http: {source: 'path'}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
returns: {
|
returns: {
|
||||||
|
@ -19,61 +18,98 @@ module.exports = function(Self) {
|
||||||
root: true
|
root: true
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
path: `/:id/makeInvoice`,
|
path: `/makeInvoice`,
|
||||||
verb: 'POST'
|
verb: 'POST'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.makeInvoice = async(ctx, id) => {
|
Self.makeInvoice = async(ctx, ticketsIds, options) => {
|
||||||
const userId = ctx.req.accessToken.userId;
|
const userId = ctx.req.accessToken.userId;
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const tx = await Self.beginTransaction({});
|
|
||||||
|
let tx;
|
||||||
|
let myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
if (!myOptions.transaction) {
|
||||||
|
tx = await Self.beginTransaction({});
|
||||||
|
myOptions.transaction = tx;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const tickets = await models.Ticket.find({
|
||||||
|
where: {
|
||||||
|
id: {inq: ticketsIds}
|
||||||
|
},
|
||||||
|
fields: ['id', 'clientFk', 'companyFk']
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
const filter = {fields: ['id', 'clientFk', 'companyFk']};
|
const [firstTicket] = tickets;
|
||||||
const ticket = await models.Ticket.findById(id, filter, options);
|
const clientId = firstTicket.clientFk;
|
||||||
|
const companyId = firstTicket.companyFk;
|
||||||
|
|
||||||
const clientCanBeInvoiced = await models.Client.canBeInvoiced(ticket.clientFk);
|
const isSameClient = tickets.every(ticket => ticket.clientFk == clientId);
|
||||||
|
if (!isSameClient)
|
||||||
|
throw new UserError(`You can't invoice tickets from multiple clients`);
|
||||||
|
|
||||||
|
const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, myOptions);
|
||||||
if (!clientCanBeInvoiced)
|
if (!clientCanBeInvoiced)
|
||||||
throw new UserError(`This client can't be invoiced`);
|
throw new UserError(`This client can't be invoiced`);
|
||||||
|
|
||||||
const ticketCanBeInvoiced = await models.Ticket.canBeInvoiced(ticket.id);
|
const ticketCanBeInvoiced = await models.Ticket.canBeInvoiced(ticketsIds, myOptions);
|
||||||
if (!ticketCanBeInvoiced)
|
if (!ticketCanBeInvoiced)
|
||||||
throw new UserError(`This ticket can't be invoiced`);
|
throw new UserError(`Some of the selected tickets are not billable`);
|
||||||
|
|
||||||
const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`;
|
const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`;
|
||||||
const [result] = await Self.rawSql(query, [ticket.clientFk, ticket.companyFk, 'R'], options);
|
const [result] = await Self.rawSql(query, [
|
||||||
|
clientId,
|
||||||
|
companyId,
|
||||||
|
'R'
|
||||||
|
], myOptions);
|
||||||
const serial = result.serial;
|
const serial = result.serial;
|
||||||
|
|
||||||
await Self.rawSql('CALL invoiceFromTicket(?)', [id], options);
|
await Self.rawSql(`
|
||||||
await Self.rawSql('CALL invoiceOut_new(?, CURDATE(), null, @invoiceId)', [serial], options);
|
DROP TEMPORARY TABLE IF EXISTS ticketToInvoice;
|
||||||
|
CREATE TEMPORARY TABLE ticketToInvoice
|
||||||
|
(PRIMARY KEY (id))
|
||||||
|
ENGINE = MEMORY
|
||||||
|
SELECT id FROM vn.ticket
|
||||||
|
WHERE id IN(?) AND refFk IS NULL
|
||||||
|
`, [ticketsIds], myOptions);
|
||||||
|
|
||||||
const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], options);
|
await Self.rawSql('CALL invoiceOut_new(?, CURDATE(), null, @invoiceId)', [serial], myOptions);
|
||||||
|
|
||||||
|
const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions);
|
||||||
|
|
||||||
const invoiceId = resultInvoice.id;
|
const invoiceId = resultInvoice.id;
|
||||||
|
|
||||||
const ticketInvoice = await models.Ticket.findById(id, {fields: ['refFk']}, options);
|
for (let ticket of tickets) {
|
||||||
|
const ticketInvoice = await models.Ticket.findById(ticket.id, {
|
||||||
|
fields: ['refFk']
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
await models.TicketLog.create({
|
await models.TicketLog.create({
|
||||||
originFk: ticket.id,
|
originFk: ticket.id,
|
||||||
userFk: userId,
|
userFk: userId,
|
||||||
action: 'insert',
|
action: 'insert',
|
||||||
changedModel: 'Ticket',
|
changedModel: 'Ticket',
|
||||||
changedModelId: ticket.id,
|
changedModelId: ticket.id,
|
||||||
newInstance: ticketInvoice
|
newInstance: ticketInvoice
|
||||||
}, options);
|
}, myOptions);
|
||||||
|
}
|
||||||
|
|
||||||
if (serial != 'R' && invoiceId) {
|
if (serial != 'R' && invoiceId) {
|
||||||
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options);
|
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], myOptions);
|
||||||
await models.InvoiceOut.createPdf(ctx, invoiceId, options);
|
await models.InvoiceOut.createPdf(ctx, invoiceId, myOptions);
|
||||||
}
|
}
|
||||||
await tx.commit();
|
|
||||||
|
|
||||||
return {invoiceFk: invoiceId, serial};
|
if (tx) await tx.commit();
|
||||||
|
|
||||||
|
return {invoiceFk: invoiceId, serial: serial};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await tx.rollback();
|
if (tx) await tx.rollback();
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
const app = require('vn-loopback/server/server');
|
||||||
|
const LoopBackContext = require('loopback-context');
|
||||||
|
const models = app.models;
|
||||||
|
|
||||||
|
describe('ticket canBeInvoiced()', () => {
|
||||||
|
const userId = 19;
|
||||||
|
const ticketId = 11;
|
||||||
|
const activeCtx = {
|
||||||
|
accessToken: {userId: userId}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async done => {
|
||||||
|
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||||
|
active: activeCtx
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy for an already invoiced ticket', async() => {
|
||||||
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const ticket = await models.Ticket.findById(ticketId, null, options);
|
||||||
|
await ticket.updateAttribute('refFk', 'T1234567', options);
|
||||||
|
|
||||||
|
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options);
|
||||||
|
|
||||||
|
expect(canBeInvoiced).toEqual(false);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy for a ticket with a price of zero', async() => {
|
||||||
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const ticket = await models.Ticket.findById(ticketId, null, options);
|
||||||
|
await ticket.updateAttribute('totalWithVat', 0, options);
|
||||||
|
|
||||||
|
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options);
|
||||||
|
|
||||||
|
expect(canBeInvoiced).toEqual(false);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return falsy for a ticket shipping in future', async() => {
|
||||||
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const ticket = await models.Ticket.findById(ticketId, null, options);
|
||||||
|
|
||||||
|
const shipped = new Date();
|
||||||
|
shipped.setDate(shipped.getDate() + 1);
|
||||||
|
|
||||||
|
await ticket.updateAttribute('shipped', shipped, options);
|
||||||
|
|
||||||
|
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options);
|
||||||
|
|
||||||
|
expect(canBeInvoiced).toEqual(false);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return truthy for an invoiceable ticket', async() => {
|
||||||
|
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId]);
|
||||||
|
|
||||||
|
expect(canBeInvoiced).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,18 +1,17 @@
|
||||||
const app = require('vn-loopback/server/server');
|
const app = require('vn-loopback/server/server');
|
||||||
const LoopBackContext = require('loopback-context');
|
const LoopBackContext = require('loopback-context');
|
||||||
|
const models = app.models;
|
||||||
|
|
||||||
describe('ticket makeInvoice()', () => {
|
describe('ticket makeInvoice()', () => {
|
||||||
const userId = 19;
|
const userId = 19;
|
||||||
|
const ticketId = 11;
|
||||||
|
const clientId = 1102;
|
||||||
const activeCtx = {
|
const activeCtx = {
|
||||||
accessToken: {userId: userId},
|
accessToken: {userId: userId},
|
||||||
headers: {origin: 'http://localhost:5000'},
|
headers: {origin: 'http://localhost:5000'},
|
||||||
};
|
};
|
||||||
const ctx = {req: activeCtx};
|
const ctx = {req: activeCtx};
|
||||||
|
|
||||||
let invoice;
|
|
||||||
let ticketId = 11;
|
|
||||||
const okState = 3;
|
|
||||||
|
|
||||||
beforeAll(async done => {
|
beforeAll(async done => {
|
||||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||||
active: activeCtx
|
active: activeCtx
|
||||||
|
@ -21,47 +20,93 @@ describe('ticket makeInvoice()', () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async done => {
|
it('should throw an error when invoicing tickets from multiple clients', async() => {
|
||||||
try {
|
const invoiceOutModel = models.InvoiceOut;
|
||||||
let ticket = await app.models.Ticket.findById(11);
|
|
||||||
await ticket.updateAttributes({refFk: null});
|
|
||||||
|
|
||||||
let ticketTrackings = await app.models.TicketTracking.find({
|
|
||||||
where: {
|
|
||||||
ticketFk: ticketId,
|
|
||||||
stateFk: {neq: okState}
|
|
||||||
},
|
|
||||||
order: 'id DESC'
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let state of ticketTrackings)
|
|
||||||
await state.destroy();
|
|
||||||
|
|
||||||
let invoiceOut = await app.models.InvoiceOut.findById(invoice.invoiceFk);
|
|
||||||
await invoiceOut.destroy();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should invoice a ticket, then try again to fail', async() => {
|
|
||||||
const invoiceOutModel = app.models.InvoiceOut;
|
|
||||||
spyOn(invoiceOutModel, 'createPdf');
|
spyOn(invoiceOutModel, 'createPdf');
|
||||||
|
|
||||||
invoice = await app.models.Ticket.makeInvoice(ctx, ticketId);
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
expect(invoice.invoiceFk).toBeDefined();
|
|
||||||
expect(invoice.serial).toEqual('T');
|
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
|
|
||||||
await app.models.Ticket.makeInvoice(ctx, ticketId).catch(e => {
|
try {
|
||||||
error = e;
|
const options = {transaction: tx};
|
||||||
}).finally(() => {
|
const otherClientTicketId = 16;
|
||||||
expect(error.message).toEqual(`This ticket can't be invoiced`);
|
await models.Ticket.makeInvoice(ctx, [ticketId, otherClientTicketId], options);
|
||||||
});
|
|
||||||
|
|
||||||
expect(error).toBeDefined();
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.message).toEqual(`You can't invoice tickets from multiple clients`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should throw an error when invoicing a client without tax data checked`, async() => {
|
||||||
|
const invoiceOutModel = models.InvoiceOut;
|
||||||
|
spyOn(invoiceOutModel, 'createPdf');
|
||||||
|
|
||||||
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
|
let error;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const client = await models.Client.findById(clientId, null, options);
|
||||||
|
await client.updateAttribute('isTaxDataChecked', false, options);
|
||||||
|
|
||||||
|
await models.Ticket.makeInvoice(ctx, [ticketId], options);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.message).toEqual(`This client can't be invoiced`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoice a ticket, then try again to fail', async() => {
|
||||||
|
const invoiceOutModel = models.InvoiceOut;
|
||||||
|
spyOn(invoiceOutModel, 'createPdf');
|
||||||
|
|
||||||
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
|
let error;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
await models.Ticket.makeInvoice(ctx, [ticketId], options);
|
||||||
|
await models.Ticket.makeInvoice(ctx, [ticketId], options);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.message).toEqual(`Some of the selected tickets are not billable`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should success to invoice a ticket', async() => {
|
||||||
|
const invoiceOutModel = models.InvoiceOut;
|
||||||
|
spyOn(invoiceOutModel, 'createPdf');
|
||||||
|
|
||||||
|
const tx = await models.Ticket.beginTransaction({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
const invoice = await models.Ticket.makeInvoice(ctx, [ticketId], options);
|
||||||
|
|
||||||
|
expect(invoice.invoiceFk).toBeDefined();
|
||||||
|
expect(invoice.serial).toEqual('T');
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -217,7 +217,8 @@ class Controller extends Section {
|
||||||
}
|
}
|
||||||
|
|
||||||
makeInvoice() {
|
makeInvoice() {
|
||||||
return this.$http.post(`Tickets/${this.id}/makeInvoice`)
|
const params = {ticketsIds: [this.id]};
|
||||||
|
return this.$http.post(`Tickets/makeInvoice`, params)
|
||||||
.then(() => this.reload())
|
.then(() => this.reload())
|
||||||
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
|
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,7 +139,8 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
|
||||||
jest.spyOn(controller, 'reload').mockReturnThis();
|
jest.spyOn(controller, 'reload').mockReturnThis();
|
||||||
jest.spyOn(controller.vnApp, 'showSuccess');
|
jest.spyOn(controller.vnApp, 'showSuccess');
|
||||||
|
|
||||||
$httpBackend.expectPOST(`Tickets/${ticket.id}/makeInvoice`).respond();
|
const expectedParams = {ticketsIds: [ticket.id]};
|
||||||
|
$httpBackend.expectPOST(`Tickets/makeInvoice`, expectedParams).respond();
|
||||||
controller.makeInvoice();
|
controller.makeInvoice();
|
||||||
$httpBackend.flush();
|
$httpBackend.flush();
|
||||||
|
|
||||||
|
|
|
@ -166,6 +166,15 @@
|
||||||
vn-tooltip="Payment on account..."
|
vn-tooltip="Payment on account..."
|
||||||
tooltip-position="left">
|
tooltip-position="left">
|
||||||
</vn-button>
|
</vn-button>
|
||||||
|
<vn-button class="round sm vn-mb-sm"
|
||||||
|
icon="icon-invoices"
|
||||||
|
ng-click="makeInvoiceConfirmation.show()"
|
||||||
|
ng-show="$ctrl.totalChecked > 0"
|
||||||
|
vn-tooltip="Make invoice..."
|
||||||
|
tooltip-position="left"
|
||||||
|
vn-acl="invoicing"
|
||||||
|
vn-acl-action="remove">
|
||||||
|
</vn-button>
|
||||||
<a ui-sref="ticket.create($ctrl.clientParams())" vn-bind="+">
|
<a ui-sref="ticket.create($ctrl.clientParams())" vn-bind="+">
|
||||||
<vn-button class="round md vn-mb-sm"
|
<vn-button class="round md vn-mb-sm"
|
||||||
icon="add"
|
icon="add"
|
||||||
|
@ -225,3 +234,11 @@
|
||||||
</vn-item>
|
</vn-item>
|
||||||
</slot-menu>
|
</slot-menu>
|
||||||
</vn-contextmenu>
|
</vn-contextmenu>
|
||||||
|
|
||||||
|
<!-- Make invoice confirmation dialog -->
|
||||||
|
<vn-confirm
|
||||||
|
vn-id="makeInvoiceConfirmation"
|
||||||
|
on-accept="$ctrl.makeInvoice()"
|
||||||
|
question="{{$ctrl.confirmationMessage}}"
|
||||||
|
message="Invoice selected tickets">
|
||||||
|
</vn-confirm>
|
|
@ -8,6 +8,7 @@ export default class Controller extends Section {
|
||||||
super($element, $);
|
super($element, $);
|
||||||
this.vnReport = vnReport;
|
this.vnReport = vnReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDelivered() {
|
setDelivered() {
|
||||||
const checkedTickets = this.checked;
|
const checkedTickets = this.checked;
|
||||||
let ids = [];
|
let ids = [];
|
||||||
|
@ -74,6 +75,14 @@ export default class Controller extends Section {
|
||||||
return this.checked.length;
|
return this.checked.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get confirmationMessage() {
|
||||||
|
if (!this.$.model) return 0;
|
||||||
|
|
||||||
|
return this.$t(`Are you sure to invoice tickets`, {
|
||||||
|
ticketsAmount: this.totalChecked
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMoreOpen() {
|
onMoreOpen() {
|
||||||
let options = this.moreOptions.filter(o => o.always || this.isChecked);
|
let options = this.moreOptions.filter(o => o.always || this.isChecked);
|
||||||
this.$.moreButton.data = options;
|
this.$.moreButton.data = options;
|
||||||
|
@ -159,6 +168,13 @@ export default class Controller extends Section {
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeInvoice() {
|
||||||
|
const ticketsIds = this.checked.map(ticket => ticket.id);
|
||||||
|
return this.$http.post(`Tickets/makeInvoice`, {ticketsIds})
|
||||||
|
.then(() => this.$.model.refresh())
|
||||||
|
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Controller.$inject = ['$element', '$scope', 'vnReport'];
|
Controller.$inject = ['$element', '$scope', 'vnReport'];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Are you sure to invoice tickets: Are you sure to invoice {{ticketsAmount}} tickets?
|
|
@ -12,3 +12,8 @@ Remove all filters: Eliminar todos los filtros
|
||||||
Copy value: Copiar valor
|
Copy value: Copiar valor
|
||||||
No verified data: Sin datos comprobados
|
No verified data: Sin datos comprobados
|
||||||
Component lack: Faltan componentes
|
Component lack: Faltan componentes
|
||||||
|
Quick invoice: Factura rápida
|
||||||
|
Multiple invoice: Factura múltiple
|
||||||
|
Make invoice...: Crear factura...
|
||||||
|
Invoice selected tickets: Facturar tickets seleccionados
|
||||||
|
Are you sure to invoice tickets: ¿Seguro que quieres facturar {{ticketsAmount}} tickets?
|
|
@ -91,8 +91,10 @@ class Controller extends Section {
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveContract() {
|
getActiveContract() {
|
||||||
this.$http.get(`Workers/${this.worker.id}/activeContract`)
|
this.$http.get(`Workers/${this.worker.id}/activeContract`).then(res => {
|
||||||
.then(res => this.businessId = res.data.businessFk);
|
if (res.data)
|
||||||
|
this.businessId = res.data.businessFk;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getContractHolidays() {
|
getContractHolidays() {
|
||||||
|
|
|
@ -90,7 +90,10 @@ class Controller extends Section {
|
||||||
|
|
||||||
getActiveContract() {
|
getActiveContract() {
|
||||||
return this.$http.get(`Workers/${this.worker.id}/activeContract`)
|
return this.$http.get(`Workers/${this.worker.id}/activeContract`)
|
||||||
.then(res => this.businessId = res.data.businessFk);
|
.then(res => {
|
||||||
|
if (res.data)
|
||||||
|
this.businessId = res.data.businessFk;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchHours() {
|
fetchHours() {
|
||||||
|
@ -111,6 +114,8 @@ class Controller extends Section {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAbsences() {
|
getAbsences() {
|
||||||
|
if (!this.businessId) return;
|
||||||
|
|
||||||
const fullYear = this.started.getFullYear();
|
const fullYear = this.started.getFullYear();
|
||||||
let params = {
|
let params = {
|
||||||
businessFk: this.businessId,
|
businessFk: this.businessId,
|
||||||
|
|
|
@ -1,163 +1,166 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html v-bind:lang="$i18n.locale">
|
<html v-bind:lang="$i18n.locale">
|
||||||
<body>
|
<body>
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<!-- Header block -->
|
<!-- Header block -->
|
||||||
<report-header v-bind="$props"></report-header>
|
<report-header v-bind="$props"></report-header>
|
||||||
<!-- Block -->
|
<!-- Block -->
|
||||||
<div class="grid-row route-block" v-for="route in routes">
|
<div class="grid-row route-block" v-for="route in routes">
|
||||||
<div class="grid-block">
|
<div class="grid-block">
|
||||||
<h1 class="title uppercase">{{$t('route')}} {{route.id}}</h1>
|
<h1 class="title uppercase">{{$t('route')}} {{route.id}}</h1>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="header">{{$t('information')}}</div>
|
<div class="header">{{$t('information')}}</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div>
|
|
||||||
<table width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th class="font gray align-right">{{$t('route')}}</th>
|
|
||||||
<td>{{route.id}}</td>
|
|
||||||
<th class="font gray align-right">{{$t('driver')}}</th>
|
|
||||||
<td>{{route.userNickName}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="font gray align-right">{{$t('date')}}</th>
|
|
||||||
<td>{{route.created | date('%d-%m-%Y')}}</td>
|
|
||||||
<th class="font gray align-right">{{$t('vehicle')}}</th>
|
|
||||||
<td>{{route.vehicleTradeMark}} {{route.vehicleModel}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="font gray align-right">{{$t('time')}}</th>
|
|
||||||
<td>{{route.time | date('%H:%M')}}</td>
|
|
||||||
<td></td>
|
|
||||||
<td>{{route.plateNumber}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="font gray align-right">{{$t('volume')}}</th>
|
|
||||||
<td>{{route.m3}}</td>
|
|
||||||
<th class="font gray align-right">{{$t('agency')}}</th>
|
|
||||||
<td>{{route.agencyName}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="contained">
|
|
||||||
<table class="middle centered" width="70%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p class="small">Hora inicio</p>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<p class="small">Hora fin</p>
|
|
||||||
</td>
|
|
||||||
<td class="gap"></td>
|
|
||||||
<td>
|
|
||||||
<p class="small">Km inicio</p>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<p class="small">Km fin</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td v-for="i in 2">
|
|
||||||
<div class="field rectangle">
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="gap"></td>
|
|
||||||
<td v-for="i in 2">
|
|
||||||
<div class="field rectangle">
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Route ticket list -->
|
|
||||||
<div class="no-page-break" v-for="ticket in route.tickets">
|
|
||||||
<div>
|
|
||||||
<table class="column-oriented repeatable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="number">{{$t('order')}}</th>
|
|
||||||
<th class="number">{{$t('ticket')}}</th>
|
|
||||||
<th width="50%">{{$t('client')}}</th>
|
|
||||||
<th class="number">{{$t('address')}}</th>
|
|
||||||
<th class="number">{{$t('packages')}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="number">{{ticket.priority}}</td>
|
|
||||||
<td class="number">{{ticket.id}}</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">
|
|
||||||
{{ticket.addressFk.toString().substr(-3, 3)}}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="number">{{ticket.packages}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<table width="100%">
|
<table width="100%">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="font gray align-right">{{$t('street')}}</th>
|
<th class="font gray align-right">{{$t('route')}}</th>
|
||||||
<td>{{ticket.street}}</td>
|
<td>{{route.id}}</td>
|
||||||
<th class="font gray align-right">{{$t('postcode')}}</th>
|
<th class="font gray align-right">{{$t('driver')}}</th>
|
||||||
<td>{{ticket.postalCode}}</td>
|
<td>{{route.userNickName}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="font gray align-right">{{$t('city')}}</th>
|
<th class="font gray align-right">{{$t('date')}}</th>
|
||||||
<td>{{ticket.city}}</td>
|
<td>{{route.created | date('%d-%m-%Y')}}</td>
|
||||||
|
<th class="font gray align-right">{{$t('vehicle')}}</th>
|
||||||
|
<td>{{route.vehicleTradeMark}} {{route.vehicleModel}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="font gray align-right">{{$t('time')}}</th>
|
||||||
|
<td>{{route.time | date('%H:%M')}}</td>
|
||||||
|
<td></td>
|
||||||
|
<td>{{route.plateNumber}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="font gray align-right">{{$t('volume')}}</th>
|
||||||
|
<td>{{route.m3}}</td>
|
||||||
<th class="font gray align-right">{{$t('agency')}}</th>
|
<th class="font gray align-right">{{$t('agency')}}</th>
|
||||||
<td>{{ticket.ticketAgency}}</td>
|
<td>{{route.agencyName}}</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="font gray align-right">{{$t('mobile')}}</th>
|
|
||||||
<td>{{ticket.mobile}}</td>
|
|
||||||
<th class="font gray align-right">{{$t('phone')}}</th>
|
|
||||||
<td>{{ticket.phone}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="font gray align-right">{{$t('warehouse')}}</th>
|
|
||||||
<td>{{ticket.warehouseName}}</td>
|
|
||||||
<th class="font gray align-right">{{$t('salesPerson')}}</th>
|
|
||||||
<td>{{ticket.salesPersonName}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="font gray align-right">{{$t('import')}}</th>
|
|
||||||
<td>{{ticket.import | currency('EUR', $i18n.locale)}}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-if="ticket.description || ticket.shipFk" class="text-area">
|
<div class="contained">
|
||||||
<p>{{ticket.description}}</p>
|
<table class="middle centered" width="70%">
|
||||||
<p v-if="ticket.shipFk">{{$t('stowaway')}}: {{ticket.shipFk}}</p>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="small">Hora inicio</p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p class="small">Hora fin</p>
|
||||||
|
</td>
|
||||||
|
<td class="gap"></td>
|
||||||
|
<td>
|
||||||
|
<p class="small">Km inicio</p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p class="small">Km fin</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td v-for="i in 2">
|
||||||
|
<div class="field rectangle">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="gap"></td>
|
||||||
|
<td v-for="i in 2">
|
||||||
|
<div class="field rectangle">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Route ticket list -->
|
||||||
|
<div class="no-page-break" v-for="ticket in route.tickets">
|
||||||
|
<div>
|
||||||
|
<table class="column-oriented repeatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="number">{{$t('order')}}</th>
|
||||||
|
<th class="number">{{$t('ticket')}}</th>
|
||||||
|
<th width="50%">{{$t('client')}}</th>
|
||||||
|
<th class="number">{{$t('address')}}</th>
|
||||||
|
<th class="number">{{$t('packages')}}</th>
|
||||||
|
<th>{{$t('packagingType')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="number">{{ticket.priority}}</td>
|
||||||
|
<td class="number">{{ticket.id}}</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">
|
||||||
|
{{ticket.addressFk.toString().substr(-3, 3)}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="number">{{ticket.packages}}</td>
|
||||||
|
<td>{{ticket.itemPackingTypes}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<table width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th class="font gray align-right">{{$t('street')}}</th>
|
||||||
|
<td>{{ticket.street}}</td>
|
||||||
|
<th class="font gray align-right">{{$t('postcode')}}</th>
|
||||||
|
<td>{{ticket.postalCode}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="font gray align-right">{{$t('city')}}</th>
|
||||||
|
<td>{{ticket.city}}</td>
|
||||||
|
<th class="font gray align-right">{{$t('agency')}}</th>
|
||||||
|
<td>{{ticket.ticketAgency}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="font gray align-right">{{$t('mobile')}}</th>
|
||||||
|
<td>{{ticket.mobile}}</td>
|
||||||
|
<th class="font gray align-right">{{$t('phone')}}</th>
|
||||||
|
<td>{{ticket.phone}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="font gray align-right">{{$t('warehouse')}}</th>
|
||||||
|
<td>{{ticket.warehouseName}}</td>
|
||||||
|
<th class="font gray align-right">{{$t('salesPerson')}}</th>
|
||||||
|
<td>{{ticket.salesPersonName}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="font gray align-right">{{$t('import')}}</th>
|
||||||
|
<td>{{ticket.import | currency('EUR', $i18n.locale)}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-if="ticket.description || ticket.shipFk" class="text-area">
|
||||||
|
<p>{{ticket.description}}</p>
|
||||||
|
<p v-if="ticket.shipFk">{{$t('stowaway')}}: {{ticket.shipFk}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Footer block -->
|
</div>
|
||||||
<report-footer id="pageFooter"
|
<!-- Footer block -->
|
||||||
v-bind:left-text="$t('routeId', [routeId])"
|
<report-footer id="pageFooter"
|
||||||
v-bind="$props">
|
v-bind:left-text="$t('routeId', [routeId])"
|
||||||
</report-footer>
|
v-bind="$props">
|
||||||
</td>
|
</report-footer>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
</body>
|
</table>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -30,7 +30,7 @@ module.exports = {
|
||||||
return this.rawSqlFromDef('routes', [routesId]);
|
return this.rawSqlFromDef('routes', [routesId]);
|
||||||
},
|
},
|
||||||
fetchTickets(routesId) {
|
fetchTickets(routesId) {
|
||||||
return this.rawSqlFromDef('tickets', [routesId]);
|
return this.rawSqlFromDef('tickets', [routesId, routesId]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -10,6 +10,7 @@ order: Orden
|
||||||
client: Cliente
|
client: Cliente
|
||||||
address: Consignatario
|
address: Consignatario
|
||||||
packages: Bultos
|
packages: Bultos
|
||||||
|
packagingType: Encajado
|
||||||
street: Dirección
|
street: Dirección
|
||||||
postcode: Código Postal
|
postcode: Código Postal
|
||||||
city: Ciudad
|
city: Ciudad
|
||||||
|
|
|
@ -18,7 +18,8 @@ SELECT
|
||||||
am.name ticketAgency,
|
am.name ticketAgency,
|
||||||
tob.description,
|
tob.description,
|
||||||
s.shipFk,
|
s.shipFk,
|
||||||
u.nickName salesPersonName
|
u.nickName salesPersonName,
|
||||||
|
ipkg.itemPackingTypes
|
||||||
FROM route r
|
FROM route r
|
||||||
LEFT JOIN ticket t ON t.routeFk = r.id
|
LEFT JOIN ticket t ON t.routeFk = r.id
|
||||||
LEFT JOIN address a ON a.id = t.addressFk
|
LEFT JOIN address a ON a.id = t.addressFk
|
||||||
|
@ -30,5 +31,15 @@ FROM route r
|
||||||
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
|
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
|
||||||
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
|
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
|
||||||
LEFT JOIN stowaway s ON s.id = t.id
|
LEFT JOIN stowaway s ON s.id = t.id
|
||||||
WHERE r.id IN(?)
|
LEFT JOIN (
|
||||||
ORDER BY t.priority, t.id
|
SELECT t.id AS ticketFk,
|
||||||
|
GROUP_CONCAT(DISTINCT(i.itemPackingTypeFk)) AS itemPackingTypes
|
||||||
|
FROM route r
|
||||||
|
JOIN ticket t ON t.routeFk = r.id
|
||||||
|
JOIN sale s ON s.ticketFk = t.id
|
||||||
|
JOIN item i ON i.id = s.itemFk
|
||||||
|
WHERE r.id IN (?)
|
||||||
|
GROUP BY t.id
|
||||||
|
) ipkg ON ipkg.ticketFk = t.id
|
||||||
|
WHERE r.id IN (?)
|
||||||
|
ORDER BY t.priority, t.id;
|
|
@ -1,9 +1,11 @@
|
||||||
SELECT
|
SELECT
|
||||||
r.id,
|
r.id,
|
||||||
r.amountPaid,
|
r.amountPaid,
|
||||||
r.amountUnpaid,
|
cr.amount AS amountUnpaid,
|
||||||
r.payed,
|
r.payed,
|
||||||
r.companyFk
|
r.companyFk
|
||||||
FROM receipt r
|
FROM receipt r
|
||||||
JOIN client c ON c.id = r.clientFk
|
JOIN client c ON c.id = r.clientFk
|
||||||
|
JOIN vn.clientRisk cr ON cr.clientFk = c.id
|
||||||
|
AND cr.companyFk = r.companyFk
|
||||||
WHERE r.id = ?
|
WHERE r.id = ?
|
Loading…
Reference in New Issue