Merge branch 'dev' into 2940-invoiceInTax-section

This commit is contained in:
Javi Gallego 2021-07-08 08:20:30 +02:00
commit 3ca2dd820d
44 changed files with 885 additions and 425 deletions

View File

@ -3,7 +3,6 @@
SCHEMAS=(
account
bs
bi
cache
edi
hedera

View File

@ -182,5 +182,7 @@
"Client assignment has changed": "He cambiado el comercial ~*\"<{{previousWorkerName}}>\"*~ por *\"<{{currentWorkerName}}>\"* del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"None": "Ninguno",
"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"
}

View File

@ -18,20 +18,15 @@
"acquireTimeout": 20000
},
"osticket": {
"connector": "vn-mysql",
"database": "vn",
"debug": false,
"host": "localhost",
"port": "3306",
"username": "root",
"password": "root"
"connector": "memory",
"timezone": "local"
},
"tempStorage": {
"name": "tempStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"provider": "filesystem",
"root": "./storage/tmp",
"maxFileSize": "262144000",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
"application/x-zip-compressed",
@ -41,17 +36,17 @@
"application/zip",
"application/rar",
"multipart/x-zip",
"image/png",
"image/jpeg",
"image/png",
"image/jpeg",
"image/jpg"
]
},
"dmsStorage": {
"name": "dmsStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "262144000",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
"application/x-zip-compressed",
@ -61,32 +56,32 @@
"application/zip",
"application/rar",
"multipart/x-zip",
"image/png",
"image/jpeg",
"image/png",
"image/jpeg",
"image/jpg"
]
},
"imageStorage": {
"name": "imageStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"provider": "filesystem",
"root": "./storage/image",
"maxFileSize": "52428800",
"maxFileSize": "52428800",
"allowedContentTypes": [
"image/png",
"image/jpeg",
"image/png",
"image/jpeg",
"image/jpg"
]
},
"invoiceStorage": {
"name": "invoiceStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"provider": "filesystem",
"root": "./storage/pdfs/invoice",
"maxFileSize": "52428800",
"maxFileSize": "52428800",
"allowedContentTypes": [
"application/octet-stream",
"application/pdf"
]
}
}
}

View File

@ -5,7 +5,7 @@ module.exports = function(Self) {
accepts: [
{
arg: 'id',
type: 'string',
type: 'number',
required: true,
description: 'Client id',
http: {source: 'path'}
@ -22,8 +22,18 @@ module.exports = function(Self) {
}
});
Self.canBeInvoiced = async id => {
let client = await Self.app.models.Client.findById(id, {fields: ['id', 'isTaxDataChecked', 'hasToInvoice']});
Self.canBeInvoiced = async(id, options) => {
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)
return true;

View File

@ -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);
});
});

View File

@ -60,7 +60,8 @@
<vn-horizontal>
<vn-input-number
ng-model="$ctrl.deliveredAmount"
label="Delivered amount">
label="Delivered amount"
step="0.01">
</vn-input-number>
<vn-input-number
disabled="true"

View File

@ -2,13 +2,16 @@
auto-load="true"
vn-id="model"
url="Entries/{{$ctrl.$params.id}}/getBuys"
data="$ctrl.buys">
data="$ctrl.buys"
limit="20">
</vn-crud-model>
<vn-watcher
vn-id="watcher"
data="$ctrl.buys">
</vn-watcher>
<div class="vn-w-xl">
<vn-data-viewer
model="model"
class="vn-w-xl">
<vn-card class="vn-pa-lg">
<div class="tableWrapper">
<vn-horizontal class="header">
@ -191,7 +194,7 @@
</div>
</div>
</vn-card>
</div>
</vn-data-viewer>
<div fixed-bottom-right>
<vn-vertical style="align-items: center;">
<a ui-sref="entry.card.buy.import"

View File

@ -10,7 +10,8 @@ describe('Entry buy', () => {
$httpBackend = _$httpBackend_;
let $element = $compile('<vn-entry-buy-index></vn-entry-buy-index')($rootScope);
controller = $componentController('vnEntryBuyIndex', {$element});
$httpBackend.whenGET('Entries//getBuys?filter=%7B%7D').respond([{id: 1}]);
const params = _$httpParamSerializer_({filter: {limit: 20}});
$httpBackend.whenGET(`Entries//getBuys?${params}`).respond([{id: 1}]);
}));
describe('saveBuy()', () => {

View File

@ -10,73 +10,73 @@ module.exports = Self => {
accepts: [
{
arg: 'filter',
type: 'Object',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
},
{
arg: 'search',
type: 'String',
type: 'string',
description: 'Searchs the invoiceOut by id',
http: {source: 'query'}
},
{
arg: 'clientFk',
type: 'Integer',
type: 'integer',
description: 'The client id',
http: {source: 'query'}
},
{
arg: 'fi',
type: 'String',
type: 'string',
description: 'The client fiscal id',
http: {source: 'query'}
},
{
arg: 'hasPdf',
type: 'Boolean',
type: 'boolean',
description: 'Whether the the invoiceOut has PDF or not',
http: {source: 'query'}
},
{
arg: 'amount',
type: 'Number',
type: 'number',
description: 'The amount filter',
http: {source: 'query'}
},
{
arg: 'min',
type: 'Number',
type: 'number',
description: 'The minimun amount flter',
http: {source: 'query'}
},
{
arg: 'max',
type: 'Number',
type: 'number',
description: 'The maximun amount flter',
http: {source: 'query'}
},
{
arg: 'issued',
type: 'Date',
type: 'date',
description: 'The issued date filter',
http: {source: 'query'}
},
{
arg: 'created',
type: 'Date',
type: 'date',
description: 'The created date filter',
http: {source: 'query'}
},
{
arg: 'dued',
type: 'Date',
type: 'date',
description: 'The due date filter',
http: {source: 'query'}
}
],
returns: {
type: ['Object'],
type: ['object'],
root: true
},
http: {

View File

@ -10,48 +10,57 @@ module.exports = Self => {
accepts: [
{
arg: 'filter',
type: 'Object',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
}, {
},
{
arg: 'tags',
type: ['Object'],
type: ['object'],
description: 'List of tags to filter with',
}, {
},
{
arg: 'search',
type: 'String',
type: 'string',
description: `If it's and integer searchs by id, otherwise it searchs by name`,
}, {
},
{
arg: 'id',
type: 'Integer',
type: 'integer',
description: 'Item id',
}, {
},
{
arg: 'categoryFk',
type: 'Integer',
type: 'integer',
description: 'Category id',
}, {
},
{
arg: 'typeFk',
type: 'Integer',
type: 'integer',
description: 'Type id',
}, {
},
{
arg: 'isActive',
type: 'Boolean',
type: 'boolean',
description: 'Whether the the item is or not active',
}, {
},
{
arg: 'salesPersonFk',
type: 'Integer',
type: 'integer',
description: 'The buyer of the item',
}, {
},
{
arg: 'description',
type: 'String',
type: 'string',
description: 'The item description',
}, {
},
{
arg: 'stemMultiplier',
type: 'Integer',
type: 'integer',
description: 'The item multiplier',
}
],
returns: {
type: ['Object'],
type: ['object'],
root: true
},
http: {
@ -60,23 +69,28 @@ module.exports = Self => {
}
});
Self.filter = async(ctx, filter) => {
let conn = Self.dataSource.connector;
Self.filter = async(ctx, filter, options) => {
const conn = Self.dataSource.connector;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
let codeWhere;
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},
fields: ['itemFk']
});
let itemIds = [];
}, myOptions);
const itemIds = [];
for (const item of items)
itemIds.push(item.itemFk);
codeWhere = {'i.id': {inq: itemIds}};
}
let where = buildFilter(ctx.args, (param, value) => {
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
@ -90,8 +104,8 @@ module.exports = Self => {
return {'i.stemMultiplier': value};
case 'typeFk':
return {'i.typeFk': value};
case 'category':
return {'ic.name': value};
case 'categoryFk':
return {'ic.id': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'origin':
@ -104,7 +118,7 @@ module.exports = Self => {
});
filter = mergeFilters(filter, {where});
let stmts = [];
const stmts = [];
let stmt;
stmt = new ParameterizedSQL(
@ -173,9 +187,10 @@ module.exports = Self => {
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(conn.makePagination(filter));
let itemsIndex = stmts.push(stmt) - 1;
let sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql);
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

@ -2,29 +2,62 @@ const app = require('vn-loopback/server/server');
describe('item filter()', () => {
it('should return 1 result filtering by id', async() => {
let filter = {};
let result = await app.models.Item.filter({args: {filter: filter, search: 1}});
const tx = await app.models.Item.beginTransaction({});
const options = {transaction: tx};
expect(result.length).toEqual(1);
expect(result[0].id).toEqual(1);
try {
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() => {
let filter = {};
let result = await app.models.Item.filter({args: {filter: filter, search: 4444444444}});
const tx = await app.models.Item.beginTransaction({});
const options = {transaction: tx};
expect(result.length).toEqual(1);
expect(result[0].id).toEqual(2);
try {
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() => {
let filter = {
order: 'isActive ASC, name',
limit: 8
};
let tags = [{value: 'medical box', tagFk: 58}];
let result = await app.models.Item.filter({args: {filter: filter, typeFk: 5, tags: tags}});
const tx = await app.models.Item.beginTransaction({});
const options = {transaction: tx};
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;
}
});
});

View File

@ -42,7 +42,7 @@
<vn-tbody>
<vn-tr
ng-class="::{
'isIn': sale.in,
'isIn': sale.invalue,
'balanceNegative': sale.balance < 0}"
ng-repeat="sale in sales"
vn-repeat-last
@ -76,7 +76,7 @@
</span>
</span>
</vn-td>
<vn-td number class="in">{{::sale.in | dashIfEmpty}}</vn-td>
<vn-td number class="in">{{::sale.invalue | dashIfEmpty}}</vn-td>
<vn-td number>{{::sale.out | dashIfEmpty}}</vn-td>
<vn-td number class="balance">
<span class="chip balanceSpan"

View File

@ -254,7 +254,7 @@ module.exports = Self => {
ENGINE = MEMORY
SELECT f.id ticketFk, f.clientFk, f.warehouseFk, f.shipped
FROM tmp.filter f
LEFT JOIN alertLevel al ON al.alertLevel = f.alertLevel
LEFT JOIN alertLevel al ON al.id = f.alertLevel
WHERE (al.code = 'FREE' OR f.alertLevel IS NULL)
AND f.shipped >= CURDATE()`);
stmts.push('CALL ticket_getProblems(FALSE)');

View File

@ -22,17 +22,20 @@
"abstract": true,
"component": "vn-order",
"description": "Orders"
}, {
},
{
"url": "/index?q",
"state": "order.index",
"component": "vn-order-index",
"description": "Orders"
}, {
},
{
"url": "/:id",
"state": "order.card",
"abstract": true,
"component": "vn-order-card"
}, {
},
{
"url": "/summary",
"state": "order.card.summary",
"component": "vn-order-summary",
@ -40,7 +43,8 @@
"params": {
"order": "$ctrl.order"
}
}, {
},
{
"url": "/catalog?q&categoryId&typeId&tagGroups",
"state": "order.card.catalog",
"component": "vn-order-catalog",
@ -48,7 +52,8 @@
"params": {
"order": "$ctrl.order"
}
}, {
},
{
"url": "/volume",
"state": "order.card.volume",
"component": "vn-order-volume",
@ -56,7 +61,8 @@
"params": {
"order": "$ctrl.order"
}
}, {
},
{
"url": "/line",
"state": "order.card.line",
"component": "vn-order-line",
@ -64,12 +70,14 @@
"params": {
"order": "$ctrl.order"
}
}, {
},
{
"url": "/create?clientFk",
"state": "order.create",
"component": "vn-order-create",
"description": "New order"
}, {
},
{
"url": "/basic-data",
"state": "order.card.basicData",
"component": "vn-order-basic-data",

View File

@ -4,11 +4,10 @@ module.exports = function(Self) {
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The ticket id',
http: {source: 'path'}
arg: 'ticketsIds',
description: 'The tickets id',
type: ['number'],
required: true
}
],
returns: {
@ -17,26 +16,44 @@ module.exports = function(Self) {
root: true
},
http: {
path: `/:id/canBeInvoiced`,
path: `/canBeInvoiced`,
verb: 'get'
}
});
Self.canBeInvoiced = async id => {
let ticket = await Self.app.models.Ticket.findById(id, {
Self.canBeInvoiced = async(ticketsIds, options) => {
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const tickets = await Self.find({
where: {
id: {inq: ticketsIds}
},
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`;
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;
return !(invalidTickets || hasSomeNegativeBase);
};
};

View File

@ -273,7 +273,7 @@ module.exports = Self => {
ENGINE = MEMORY
SELECT f.id ticketFk, f.clientFk, f.warehouseFk, f.shipped
FROM tmp.filter f
LEFT JOIN alertLevel al ON al.alertLevel = f.alertLevel
LEFT JOIN alertLevel al ON al.id = f.alertLevel
WHERE (al.code = 'FREE' OR f.alertLevel IS NULL)
AND f.shipped >= CURDATE()`);
stmts.push('CALL ticket_getProblems(FALSE)');

View File

@ -2,15 +2,14 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
Self.remoteMethodCtx('makeInvoice', {
description: 'Make out an invoice from a ticket id',
description: 'Make out an invoice from one or more tickets',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'string',
required: true,
description: 'Ticket id',
http: {source: 'path'}
arg: 'ticketsIds',
description: 'The tickets id',
type: ['number'],
required: true
}
],
returns: {
@ -19,61 +18,98 @@ module.exports = function(Self) {
root: true
},
http: {
path: `/:id/makeInvoice`,
path: `/makeInvoice`,
verb: 'POST'
}
});
Self.makeInvoice = async(ctx, id) => {
Self.makeInvoice = async(ctx, ticketsIds, options) => {
const userId = ctx.req.accessToken.userId;
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 {
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 ticket = await models.Ticket.findById(id, filter, options);
const [firstTicket] = tickets;
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)
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)
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 [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;
await Self.rawSql('CALL invoiceFromTicket(?)', [id], options);
await Self.rawSql('CALL invoiceOut_new(?, CURDATE(), null, @invoiceId)', [serial], options);
await Self.rawSql(`
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 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({
originFk: ticket.id,
userFk: userId,
action: 'insert',
changedModel: 'Ticket',
changedModelId: ticket.id,
newInstance: ticketInvoice
}, options);
await models.TicketLog.create({
originFk: ticket.id,
userFk: userId,
action: 'insert',
changedModel: 'Ticket',
changedModelId: ticket.id,
newInstance: ticketInvoice
}, myOptions);
}
if (serial != 'R' && invoiceId) {
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options);
await models.InvoiceOut.createPdf(ctx, invoiceId, options);
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], myOptions);
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) {
await tx.rollback();
if (tx) await tx.rollback();
throw e;
}
};

View File

@ -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);
});
});

View File

@ -1,18 +1,17 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
const models = app.models;
describe('ticket makeInvoice()', () => {
const userId = 19;
const ticketId = 11;
const clientId = 1102;
const activeCtx = {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
};
const ctx = {req: activeCtx};
let invoice;
let ticketId = 11;
const okState = 3;
beforeAll(async done => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
@ -21,47 +20,93 @@ describe('ticket makeInvoice()', () => {
done();
});
afterAll(async done => {
try {
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;
it('should throw an error when invoicing tickets from multiple clients', async() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'createPdf');
invoice = await app.models.Ticket.makeInvoice(ctx, ticketId);
expect(invoice.invoiceFk).toBeDefined();
expect(invoice.serial).toEqual('T');
const tx = await models.Ticket.beginTransaction({});
let error;
await app.models.Ticket.makeInvoice(ctx, ticketId).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`This ticket can't be invoiced`);
});
try {
const options = {transaction: tx};
const otherClientTicketId = 16;
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();
}
});
});

View File

@ -9,13 +9,12 @@
},
"properties": {
"code": {
"type": "String",
"type": "string"
},
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"alertLevel": {
"type": "Number",
"id": true
}
},
"acls": [

View File

@ -9,23 +9,23 @@
"properties": {
"id": {
"id": true,
"type": "Number",
"type": "number",
"forceId": false
},
"name": {
"type": "String",
"type": "string",
"required": false
},
"order": {
"type": "Number",
"type": "number",
"required": false
},
"alertLevel": {
"type": "Number",
"type": "number",
"required": false
},
"code": {
"type": "String",
"type": "string",
"required": false
}
}

View File

@ -9,14 +9,14 @@
"properties": {
"ticketFk": {
"id": true,
"type": "Number",
"type": "number",
"forceId": false
},
"updated": {
"type": "Date"
"type": "date"
},
"alertLevel": {
"type": "Number"
"type": "number"
},
"code": {
"type": "string"

View File

@ -19,6 +19,9 @@
ng-model="$ctrl.clientId"
initial-data="$ctrl.clientId"
order="id">
<tpl-item>
#{{id}} - {{::name}}
</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-one
required="true"

View File

@ -217,7 +217,8 @@ class Controller extends Section {
}
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.vnApp.showSuccess(this.$t('Ticket invoiced')));
}

View File

@ -139,7 +139,8 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
jest.spyOn(controller, 'reload').mockReturnThis();
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();
$httpBackend.flush();

View File

@ -166,6 +166,15 @@
vn-tooltip="Payment on account..."
tooltip-position="left">
</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="+">
<vn-button class="round md vn-mb-sm"
icon="add"
@ -224,4 +233,12 @@
Copy value
</vn-item>
</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>

View File

@ -8,6 +8,7 @@ export default class Controller extends Section {
super($element, $);
this.vnReport = vnReport;
}
setDelivered() {
const checkedTickets = this.checked;
let ids = [];
@ -74,6 +75,14 @@ export default class Controller extends Section {
return this.checked.length;
}
get confirmationMessage() {
if (!this.$.model) return 0;
return this.$t(`Are you sure to invoice tickets`, {
ticketsAmount: this.totalChecked
});
}
onMoreOpen() {
let options = this.moreOptions.filter(o => o.always || this.isChecked);
this.$.moreButton.data = options;
@ -159,6 +168,13 @@ export default class Controller extends Section {
}
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'];

View File

@ -0,0 +1 @@
Are you sure to invoice tickets: Are you sure to invoice {{ticketsAmount}} tickets?

View File

@ -11,4 +11,9 @@ Remove filter: Quitar filtro por selección
Remove all filters: Eliminar todos los filtros
Copy value: Copiar valor
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?

View File

@ -22,7 +22,7 @@
<vn-card>
<section class="vn-pa-md">
<vn-tool-bar class="vn-mb-md">
<vn-button disabled="!model.userParams.landedFrom || !model.userParams.landedTo"
<vn-button disabled="!$ctrl.hasDateRange"
icon="picture_as_pdf"
ng-click="$ctrl.showReport()"
vn-tooltip="Open as PDF">

View File

@ -32,6 +32,14 @@ class Controller extends Section {
};
}
get hasDateRange() {
const userParams = this.$.model.userParams;
const hasLanded = userParams.landedFrom || userParams.landedTo;
const hasShipped = userParams.shippedFrom || userParams.shippedTo;
return hasLanded || hasShipped;
}
findDraggable($event) {
const target = $event.target;
const draggable = target.closest(this.draggableElement);

View File

@ -14,6 +14,17 @@ describe('Travel Component vnTravelExtraCommunity', () => {
controller.$.model.refresh = jest.fn();
}));
describe('hasDateRange()', () => {
it('should return truthy when shippedFrom or landedTo are set as userParams', () => {
const now = new Date();
controller.$.model.userParams = {shippedFrom: now, landedTo: now};
const result = controller.hasDateRange;
expect(result).toBeTruthy();
});
});
describe('findDraggable()', () => {
it('should find the draggable element', () => {
const draggable = document.createElement('a');

View File

@ -91,8 +91,10 @@ class Controller extends Section {
}
getActiveContract() {
this.$http.get(`Workers/${this.worker.id}/activeContract`)
.then(res => this.businessId = res.data.businessFk);
this.$http.get(`Workers/${this.worker.id}/activeContract`).then(res => {
if (res.data)
this.businessId = res.data.businessFk;
});
}
getContractHolidays() {

View File

@ -90,7 +90,10 @@ class Controller extends Section {
getActiveContract() {
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() {
@ -111,6 +114,8 @@ class Controller extends Section {
}
getAbsences() {
if (!this.businessId) return;
const fullYear = this.started.getFullYear();
let params = {
businessFk: this.businessId,

View File

@ -5,24 +5,28 @@
</vn-zone-calendar>
</div>
<vn-side-menu side="right">
<form ng-submit="$ctrl.onSubmit()" class="vn-pa-md">
<form ng-submit="$ctrl.fetchData()" class="vn-pa-md">
<vn-radio
label="Pick up"
val="pickUp"
ng-model="$ctrl.deliveryMethodFk">
ng-model="$ctrl.deliveryMethodFk"
on-change="$ctrl.agencyModeFk = null"
tabindex="-1">
</vn-radio>
<vn-radio
label="Delivery"
val="delivery"
ng-model="$ctrl.deliveryMethodFk"
class="vn-mb-sm">
on-change="$ctrl.agencyModeFk = null"
class="vn-mb-sm"
tabindex="-1">
</vn-radio>
<vn-autocomplete
vn-one
ng-if="$ctrl.deliveryMethodFk === 'delivery'"
vn-focus
label="Postcode"
ng-model="params.geoFk"
ng-model="$ctrl.geoFk"
url="Postcodes/location"
fields="['code','townFk']"
order="code, townFk"
@ -39,7 +43,7 @@
</vn-autocomplete>
<vn-autocomplete
label="{{$ctrl.deliveryMethodFk == 'delivery' ? 'Agency' : 'Warehouse'}}"
ng-model="params.agencyModeFk"
ng-model="$ctrl.agencyModeFk"
url="AgencyModes/isActive"
where="$ctrl.agencyFilter"
vn-id="agencymode">

View File

@ -4,20 +4,44 @@ import './style.scss';
class Controller extends Section {
$onInit() {
this.$.params = {};
this.setParams();
}
$postLink() {
this.deliveryMethodFk = 'delivery';
}
onSubmit() {
setParams() {
const hasParams = this.$params.deliveryMethodFk || this.$params.geoFk || this.$params.agencyModeFk;
if (hasParams) {
if (this.$params.deliveryMethodFk)
this.deliveryMethodFk = this.$params.deliveryMethodFk;
if (this.$params.geoFk)
this.geoFk = this.$params.geoFk;
if (this.$params.agencyModeFk)
this.agencyModeFk = this.$params.agencyModeFk;
this.fetchData();
}
}
fetchData() {
const params = {
deliveryMethodFk: this.deliveryMethodFk,
geoFk: this.geoFk,
agencyModeFk: this.agencyModeFk
};
this.$.data = null;
this.$http.get(`Zones/getEvents`, {params: this.$.params})
this.$http.get(`Zones/getEvents`, {params})
.then(res => {
let data = res.data;
this.$.data = data;
if (!data.events.length)
this.vnApp.showMessage(this.$t('No service for the specified zone'));
this.$state.go(this.$state.current.name, params);
});
}
@ -27,16 +51,16 @@ class Controller extends Section {
set deliveryMethodFk(value) {
this._deliveryMethodFk = value;
this.$.params.agencyModeFk = null;
let filter;
if (value === 'pickUp') {
if (value === 'pickUp')
filter = {where: {code: 'PICKUP'}};
this.$.agencymode.focus();
} else
else
filter = {where: {code: {inq: ['DELIVERY', 'AGENCY']}}};
this.$http.get(`DeliveryMethods`, {filter}).then(res => {
let deliveryMethods = res.data.map(deliveryMethod => deliveryMethod.id);
const deliveryMethods = res.data.map(deliveryMethod => deliveryMethod.id);
this.agencyFilter = {deliveryMethodFk: {inq: deliveryMethods}};
});
}

View File

@ -19,46 +19,67 @@ describe('Zone Component vnZoneDeliveryDays', () => {
}));
describe('deliveryMethodFk() setter', () => {
it(`should set the deliveryMethodFk property and check just agencymode focus`, () => {
controller.$.agencymode = {focus: jasmine.createSpy('focus')};
it('should set the deliveryMethodFk property as pickup and then perform a query that sets the filter', () => {
$httpBackend.expect('GET', 'DeliveryMethods').respond([{id: 999}]);
controller.deliveryMethodFk = 'pickUp';
expect(controller.$.agencymode.focus).toHaveBeenCalledWith();
});
it(`should set the deliveryMethodFk property, call method http and sets the agencyfilter`, () => {
$httpBackend.when('GET', 'DeliveryMethods').respond([{id: 'id'}]);
controller.deliveryMethodFk = 'no pickUp';
$httpBackend.flush();
expect(controller.agencyFilter).toEqual({deliveryMethodFk: {inq: ['id']}});
expect(controller.agencyFilter).toEqual({deliveryMethodFk: {inq: [999]}});
});
});
describe('onSubmit()', () => {
it('should make an HTTP GET query and then call the showMessage() method', () => {
jest.spyOn(controller.vnApp, 'showMessage');
describe('setParams()', () => {
it('should do nothing when no params are received', () => {
controller.setParams();
const expectedData = {events: []};
$httpBackend.when('GET', 'Zones/getEvents').respond({events: []});
controller.onSubmit();
$httpBackend.flush();
expect(controller.$.data).toBeDefined();
expect(controller.$.data).toEqual(expectedData);
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('No service for the specified zone');
expect(controller.deliveryMethodFk).toBeUndefined();
expect(controller.geoFk).toBeUndefined();
expect(controller.agencyModeFk).toBeUndefined();
});
it('should make an HTTP GET query and then set the data property', () => {
const expectedData = {events: [{zoneFk: 1}]};
$httpBackend.when('GET', 'Zones/getEvents').respond({events: [{zoneFk: 1}]});
controller.onSubmit();
it('should set the controller properties when the params are provided', () => {
controller.$params = {
deliveryMethodFk: 3,
geoFk: 2,
agencyModeFk: 1
};
controller.setParams();
expect(controller.deliveryMethodFk).toEqual(controller.$params.deliveryMethodFk);
expect(controller.geoFk).toEqual(controller.$params.geoFk);
expect(controller.agencyModeFk).toEqual(controller.$params.agencyModeFk);
});
});
describe('fetchData()', () => {
it('should make an HTTP GET query and then call the showMessage() method', () => {
jest.spyOn(controller.vnApp, 'showMessage');
jest.spyOn(controller.$state, 'go');
controller.agencyModeFk = 1;
controller.deliveryMethodFk = 2;
controller.geoFk = 3;
controller.$state.current.name = 'myState';
const expectedData = {events: []};
const url = 'Zones/getEvents?agencyModeFk=1&deliveryMethodFk=2&geoFk=3';
$httpBackend.when('GET', 'DeliveryMethods').respond([]);
$httpBackend.expect('GET', url).respond({events: []});
controller.fetchData();
$httpBackend.flush();
expect(controller.$.data).toBeDefined();
expect(controller.$.data).toEqual(expectedData);
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('No service for the specified zone');
expect(controller.$state.go).toHaveBeenCalledWith(
controller.$state.current.name,
{
agencyModeFk: 1,
deliveryMethodFk: 2,
geoFk: 3
}
);
});
});

View File

@ -28,33 +28,39 @@
"abstract": true,
"component": "vn-zone",
"description": "Zones"
}, {
},
{
"url": "/index?q",
"state": "zone.index",
"component": "vn-zone-index",
"description": "Zones"
}, {
"url": "/delivery-days?q",
},
{
"url": "/delivery-days?q&deliveryMethodFk&geoFk&agencyModeFk",
"state": "zone.deliveryDays",
"component": "vn-zone-delivery-days",
"description": "Delivery days"
}, {
},
{
"url": "/upcoming-deliveries",
"state": "zone.upcomingDeliveries",
"component": "vn-upcoming-deliveries",
"description": "Upcoming deliveries"
}, {
},
{
"url": "/create",
"state": "zone.create",
"component": "vn-zone-create",
"description": "New zone"
}, {
},
{
"url": "/:id",
"state": "zone.card",
"component": "vn-zone-card",
"abstract": true,
"description": "Detail"
}, {
},
{
"url": "/summary",
"state": "zone.card.summary",
"component": "vn-zone-summary",
@ -62,7 +68,8 @@
"params": {
"zone": "$ctrl.zone"
}
}, {
},
{
"url": "/basic-data",
"state": "zone.card.basicData",
"component": "vn-zone-basic-data",
@ -70,17 +77,20 @@
"params": {
"zone": "$ctrl.zone"
}
}, {
},
{
"url": "/warehouses",
"state": "zone.card.warehouses",
"component": "vn-zone-warehouses",
"description": "Warehouses"
}, {
},
{
"url": "/events",
"state": "zone.card.events",
"component": "vn-zone-events",
"description": "Calendar"
}, {
},
{
"url": "/location?q",
"state": "zone.card.location",
"component": "vn-zone-location",
@ -88,7 +98,8 @@
"params": {
"zone": "$ctrl.zone"
}
}, {
},
{
"url" : "/log",
"state": "zone.card.log",
"component": "vn-zone-log",

View File

@ -21,7 +21,7 @@ module.exports = app => {
JOIN ticket t ON t.id = e.ticketFk
JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
@ -33,7 +33,7 @@ module.exports = app => {
await db.rawSql(`
UPDATE ticket t
JOIN ticketState ts ON t.id = ts.ticketFk
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
JOIN zone z ON z.id = t.zoneFk
@ -64,7 +64,7 @@ module.exports = app => {
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.id = ?
AND t.refFk IS NULL
@ -100,7 +100,7 @@ module.exports = app => {
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.agencyModeFk IN(?)
AND t.warehouseFk = ?
@ -137,7 +137,7 @@ module.exports = app => {
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.routeFk = ?
AND t.refFk IS NULL

View File

@ -1,163 +1,166 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Header block -->
<report-header v-bind="$props"></report-header>
<!-- Block -->
<div class="grid-row route-block" v-for="route in routes">
<div class="grid-block">
<h1 class="title uppercase">{{$t('route')}} {{route.id}}</h1>
<div class="panel">
<div class="header">{{$t('information')}}</div>
<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>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Header block -->
<report-header v-bind="$props"></report-header>
<!-- Block -->
<div class="grid-row route-block" v-for="route in routes">
<div class="grid-block">
<h1 class="title uppercase">{{$t('route')}} {{route.id}}</h1>
<div class="panel">
<div class="header">{{$t('information')}}</div>
<div class="body">
<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>
<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('city')}}</th>
<td>{{ticket.city}}</td>
<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>{{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>
<td>{{route.agencyName}}</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 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>
<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>
<!-- Footer block -->
<report-footer id="pageFooter"
v-bind:left-text="$t('routeId', [routeId])"
v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</div>
<!-- Footer block -->
<report-footer id="pageFooter"
v-bind:left-text="$t('routeId', [routeId])"
v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -30,7 +30,7 @@ module.exports = {
return this.rawSqlFromDef('routes', [routesId]);
},
fetchTickets(routesId) {
return this.rawSqlFromDef('tickets', [routesId]);
return this.rawSqlFromDef('tickets', [routesId, routesId]);
}
},
components: {

View File

@ -10,6 +10,7 @@ order: Orden
client: Cliente
address: Consignatario
packages: Bultos
packagingType: Encajado
street: Dirección
postcode: Código Postal
city: Ciudad

View File

@ -18,8 +18,9 @@ SELECT
am.name ticketAgency,
tob.description,
s.shipFk,
u.nickName salesPersonName
FROM route r
u.nickName salesPersonName,
ipkg.itemPackingTypes
FROM route r
LEFT JOIN ticket t ON t.routeFk = r.id
LEFT JOIN address a ON a.id = t.addressFk
LEFT JOIN client c ON c.id = t.clientFk
@ -30,5 +31,15 @@ FROM route r
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN stowaway s ON s.id = t.id
WHERE r.id IN(?)
ORDER BY t.priority, t.id
LEFT JOIN (
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;

View File

@ -1,9 +1,11 @@
SELECT
r.id,
r.amountPaid,
r.amountUnpaid,
cr.amount AS amountUnpaid,
r.payed,
r.companyFk
FROM receipt r
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 = ?