2824-ticket_totalWith_and_without_VAT #571

Merged
carlosjr merged 4 commits from 2824-ticket_totalWith_and_without_VAT into dev 2021-03-11 16:08:48 +00:00
22 changed files with 65 additions and 248 deletions

View File

@ -2242,4 +2242,9 @@ INSERT INTO `vn`.`duaInvoiceIn`(`id`, `duaFk`, `invoiceInFk`)
(7, 7, 7), (7, 7, 7),
(8, 8, 8), (8, 8, 8),
(9, 9, 9), (9, 9, 9),
(10, 10, 10); (10, 10, 10);
INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
SELECT `id` FROM `vn`.`ticket`;
CALL `vn`.`ticket_doRecalc`();

View File

@ -37,11 +37,11 @@ module.exports = Self => {
}, { }, {
arg: 'active', arg: 'active',
type: 'Boolean', type: 'Boolean',
description: 'Whether the the item is or not active', description: 'Whether the item is or not active',
}, { }, {
arg: 'visible', arg: 'visible',
type: 'Boolean', type: 'Boolean',
description: 'Whether the the item is or not visible', description: 'Whether the item is or not visible',
}, { }, {
arg: 'typeFk', arg: 'typeFk',
type: 'Integer', type: 'Integer',

View File

@ -1,13 +1,13 @@
module.exports = function(Self) { module.exports = function(Self) {
Self.remoteMethodCtx('canBeInvoiced', { Self.remoteMethodCtx('canBeInvoiced', {
description: 'Change property isEqualizated in all client addresses', description: 'Whether the ticket can or not be invoiced',
accessType: 'READ', accessType: 'READ',
accepts: [ accepts: [
{ {
arg: 'id', arg: 'id',
type: 'string', type: 'number',
required: true, required: true,
description: 'Client id', description: 'The ticket id',
http: {source: 'path'} http: {source: 'path'}
} }
], ],
@ -23,8 +23,9 @@ module.exports = function(Self) {
}); });
Self.canBeInvoiced = async id => { Self.canBeInvoiced = async id => {
let ticket = await Self.app.models.Ticket.findById(id, {fields: ['id', 'refFk', 'shipped']}); let ticket = await Self.app.models.Ticket.findById(id, {
let ticketTotal = await Self.app.models.Ticket.getTotal(id); fields: ['id', 'refFk', 'shipped', 'totalWithVat']
});
let query = `SELECT vn.hasSomeNegativeBase(?) AS hasSomeNegativeBase`; let query = `SELECT vn.hasSomeNegativeBase(?) AS hasSomeNegativeBase`;
let [result] = await Self.rawSql(query, [id]); let [result] = await Self.rawSql(query, [id]);
@ -33,7 +34,7 @@ module.exports = function(Self) {
let today = new Date(); let today = new Date();
let shipped = new Date(ticket.shipped); let shipped = new Date(ticket.shipped);
if (ticket.refFk || ticketTotal == 0 || shipped.getTime() > today.getTime() || hasSomeNegativeBase) if (ticket.refFk || ticket.totalWithVat == 0 || shipped.getTime() > today.getTime() || hasSomeNegativeBase)
return false; return false;
return true; return true;

View File

@ -203,6 +203,8 @@ module.exports = Self => {
t.routeFk, t.routeFk,
t.warehouseFk, t.warehouseFk,
t.clientFk, t.clientFk,
t.totalWithoutVat,
t.totalWithVat,
io.id AS invoiceOutId, io.id AS invoiceOutId,
a.provinceFk, a.provinceFk,
p.name AS province, p.name AS province,
@ -246,6 +248,7 @@ module.exports = Self => {
stmts.push(stmt); stmts.push(stmt);
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticketGetProblems'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticketGetProblems');
stmts.push(` stmts.push(`
CREATE TEMPORARY TABLE tmp.ticketGetProblems CREATE TEMPORARY TABLE tmp.ticketGetProblems
(INDEX (ticketFk)) (INDEX (ticketFk))
@ -255,23 +258,15 @@ module.exports = Self => {
LEFT JOIN alertLevel al ON al.alertLevel = f.alertLevel LEFT JOIN alertLevel al ON al.alertLevel = f.alertLevel
WHERE (al.code = 'FREE' OR f.alertLevel IS NULL) WHERE (al.code = 'FREE' OR f.alertLevel IS NULL)
AND f.shipped >= CURDATE()`); AND f.shipped >= CURDATE()`);
stmts.push('CALL ticketGetProblems()');
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticket'); stmts.push('CALL ticketGetProblems()');
stmts.push(`
CREATE TEMPORARY TABLE tmp.ticket
(INDEX (ticketFk)) ENGINE = MEMORY
SELECT id ticketFk FROM tmp.filter`);
stmts.push('CALL ticketGetTotal()');
stmt = new ParameterizedSQL(` stmt = new ParameterizedSQL(`
SELECT SELECT
f.*, f.*,
tt.total,
tp.* tp.*
FROM tmp.filter f FROM tmp.filter f
LEFT JOIN tmp.ticketProblems tp ON tp.ticketFk = f.id LEFT JOIN tmp.ticketProblems tp ON tp.ticketFk = f.id`);
LEFT JOIN tmp.ticketTotal tt ON tt.ticketFk = f.id`);
if (args.problems != undefined && (!args.from && !args.to)) if (args.problems != undefined && (!args.from && !args.to))
throw new UserError('Choose a date range or days forward'); throw new UserError('Choose a date range or days forward');

View File

@ -1,28 +0,0 @@
module.exports = Self => {
Self.remoteMethod('getTaxes', {
description: 'Returns ticket taxes',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'ticket id',
http: {source: 'path'}
}],
returns: {
type: 'number',
root: true
},
http: {
path: `/:id/getTaxes`,
verb: 'GET'
}
});
Self.getTaxes = async ticketFk => {
let query = `CALL vn.ticketGetTaxAdd(?)`;
let [taxes] = await Self.rawSql(query, [ticketFk]);
return taxes;
};
};

View File

@ -1,28 +0,0 @@
module.exports = Self => {
Self.remoteMethod('getTotal', {
description: 'Returns the total of a ticket',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'ticket id',
http: {source: 'path'}
}],
returns: {
type: 'number',
root: true
},
http: {
path: `/:id/getTotal`,
verb: 'GET'
}
});
Self.getTotal = async ticketFk => {
let query = `SELECT vn.ticketGetTotal(?) AS amount`;
let [total] = await Self.rawSql(query, [ticketFk]);
return total.amount ? total.amount : 0.00;
};
};

View File

@ -1,32 +0,0 @@
module.exports = Self => {
Self.remoteMethod('getVAT', {
description: 'Returns ticket total VAT',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'ticket id',
http: {source: 'path'}
}],
returns: {
type: 'number',
root: true
},
http: {
path: `/:id/getVAT`,
verb: 'GET'
}
});
Self.getVAT = async ticketFk => {
let totalTax = 0.00;
let taxes = await Self.app.models.Ticket.getTaxes(ticketFk);
taxes.forEach(tax => {
totalTax += tax.tax;
});
return Math.round(totalTax * 100) / 100;
};
};

View File

@ -1,9 +0,0 @@
const app = require('vn-loopback/server/server');
describe('ticket getTaxes()', () => {
it('should return the tax of a given ticket', async() => {
let result = await app.models.Ticket.getTaxes(1);
expect(result[0].tax).toEqual(7.1);
});
});

View File

@ -1,15 +0,0 @@
const app = require('vn-loopback/server/server');
describe('ticket getTotal()', () => {
it('should return the total of a ticket', async() => {
let result = await app.models.Ticket.getTotal(1);
expect(result).toEqual(891.87);
});
it(`should return zero if the ticket doesn't have lines`, async() => {
let result = await app.models.Ticket.getTotal(21);
expect(result).toEqual(0);
});
});

View File

@ -1,10 +0,0 @@
const app = require('vn-loopback/server/server');
describe('ticket getVAT()', () => {
it('should return the ticket VAT', async() => {
await app.models.Ticket.getVAT(1)
.then(response => {
expect(response).toEqual(84.64);
});
});
});

View File

@ -1,15 +0,0 @@
const app = require('vn-loopback/server/server');
describe('ticket subtotal()', () => {
it('should return the subtotal of a ticket', async() => {
let result = await app.models.Ticket.subtotal(1);
expect(result).toEqual(809.23);
});
it(`should return 0 if the ticket doesn't have lines`, async() => {
let result = await app.models.Ticket.subtotal(21);
expect(result).toEqual(0.00);
});
});

View File

@ -14,23 +14,15 @@ describe('ticket summary()', () => {
expect(result.sales.length).toEqual(4); expect(result.sales.length).toEqual(4);
}); });
it('should return a summary object containing subtotal for 1 ticket', async() => { it('should return a summary object containing totalWithoutVat for 1 ticket', async() => {
let result = await app.models.Ticket.summary(1); let result = await app.models.Ticket.summary(1);
expect(Math.round(result.subtotal * 100) / 100).toEqual(809.23); expect(result.totalWithoutVat).toEqual(807.23);
});
it('should return a summary object containing VAT for 1 ticket', async() => {
let result = await app.models.Ticket.summary(1);
expect(Math.round(result.vat * 100) / 100).toEqual(84.64);
}); });
it('should return a summary object containing total for 1 ticket', async() => { it('should return a summary object containing total for 1 ticket', async() => {
let result = await app.models.Ticket.summary(1); let result = await app.models.Ticket.summary(1);
let total = result.subtotal + result.vat;
let expectedTotal = Math.round(total * 100) / 100;
expect(result.total).toEqual(expectedTotal); expect(result.totalWithVat).toEqual(891.87);
}); });
}); });

View File

@ -1,39 +0,0 @@
module.exports = Self => {
Self.remoteMethod('subtotal', {
description: 'Returns the total of a ticket',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'ticket id',
http: {source: 'path'}
}],
returns: {
type: 'number',
root: true
},
http: {
path: `/:id/subtotal`,
verb: 'GET'
}
});
Self.subtotal = async ticketFk => {
const sale = Self.app.models.Sale;
const ticketSales = await sale.find({where: {ticketFk}});
const ticketService = Self.app.models.TicketService;
const ticketServices = await ticketService.find({where: {ticketFk}});
let subtotal = 0.00;
ticketSales.forEach(sale => {
subtotal += sale.price * sale.quantity * ((100 - sale.discount) / 100);
});
ticketServices.forEach(service => {
subtotal += service.price * service.quantity;
});
return Math.round(subtotal * 100) / 100;
};
};

View File

@ -23,9 +23,6 @@ module.exports = Self => {
let models = Self.app.models; let models = Self.app.models;
let summaryObj = await getTicketData(Self, ticketFk); let summaryObj = await getTicketData(Self, ticketFk);
summaryObj.sales = await getSales(models.Sale, ticketFk); summaryObj.sales = await getSales(models.Sale, ticketFk);
summaryObj.subtotal = await models.Ticket.subtotal(ticketFk);
summaryObj.vat = await models.Ticket.getVAT(ticketFk);
summaryObj.total = summaryObj.subtotal + summaryObj.vat;
summaryObj.packagings = await models.TicketPackaging.find({ summaryObj.packagings = await models.TicketPackaging.find({
where: {ticketFk: ticketFk}, where: {ticketFk: ticketFk},
include: [{relation: 'packaging', include: [{relation: 'packaging',

View File

@ -6,16 +6,12 @@ module.exports = Self => {
require('../methods/ticket/getVolume')(Self); require('../methods/ticket/getVolume')(Self);
require('../methods/ticket/getTotalVolume')(Self); require('../methods/ticket/getTotalVolume')(Self);
require('../methods/ticket/summary')(Self); require('../methods/ticket/summary')(Self);
require('../methods/ticket/getTotal')(Self);
require('../methods/ticket/getTaxes')(Self);
require('../methods/ticket/subtotal')(Self);
require('../methods/ticket/priceDifference')(Self); require('../methods/ticket/priceDifference')(Self);
require('../methods/ticket/componentUpdate')(Self); require('../methods/ticket/componentUpdate')(Self);
require('../methods/ticket/new')(Self); require('../methods/ticket/new')(Self);
require('../methods/ticket/isEditable')(Self); require('../methods/ticket/isEditable')(Self);
require('../methods/ticket/setDeleted')(Self); require('../methods/ticket/setDeleted')(Self);
require('../methods/ticket/restore')(Self); require('../methods/ticket/restore')(Self);
require('../methods/ticket/getVAT')(Self);
require('../methods/ticket/getSales')(Self); require('../methods/ticket/getSales')(Self);
require('../methods/ticket/getSalesPersonMana')(Self); require('../methods/ticket/getSalesPersonMana')(Self);
require('../methods/ticket/filter')(Self); require('../methods/ticket/filter')(Self);

View File

@ -12,30 +12,30 @@
"properties": { "properties": {
"id": { "id": {
"id": true, "id": true,
"type": "Number", "type": "number",
"description": "Identifier" "description": "Identifier"
}, },
"shipped": { "shipped": {
"type": "Date", "type": "date",
"required": true "required": true
}, },
"landed": { "landed": {
"type": "Date" "type": "date"
}, },
"nickname": { "nickname": {
"type": "String" "type": "string"
}, },
"location": { "location": {
"type": "String" "type": "string"
}, },
"solution": { "solution": {
"type": "String" "type": "string"
}, },
"packages": { "packages": {
"type": "Number" "type": "number"
}, },
"updated": { "updated": {
"type": "Date", "type": "date",
"mysql": { "mysql": {
"columnName": "created" "columnName": "created"
} }
@ -44,16 +44,22 @@
"type": "boolean" "type": "boolean"
}, },
"priority": { "priority": {
"type": "Number" "type": "number"
}, },
"zoneFk": { "zoneFk": {
"type": "Number" "type": "number"
}, },
"zonePrice": { "zonePrice": {
"type": "Number" "type": "number"
}, },
"zoneBonus": { "zoneBonus": {
"type": "Number" "type": "number"
},
"totalWithVat": {
"type": "number"
},
"totalWithoutVat": {
"type": "number"
} }
}, },
"relations": { "relations": {

View File

@ -121,7 +121,7 @@
<vn-td>{{::ticket.warehouse}}</vn-td> <vn-td>{{::ticket.warehouse}}</vn-td>
<vn-td number> <vn-td number>
<span class="chip {{$ctrl.totalPriceColor(ticket)}}"> <span class="chip {{$ctrl.totalPriceColor(ticket)}}">
{{::ticket.total | currency: 'EUR': 2}} {{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}}
</span> </span>
</vn-td> </vn-td>
<vn-td actions> <vn-td actions>

View File

@ -49,7 +49,7 @@ export default class Controller extends Section {
throw new UserError('You cannot make a payment on account from multiple clients'); throw new UserError('You cannot make a payment on account from multiple clients');
for (let ticket of checkedTickets) { for (let ticket of checkedTickets) {
this.$.balanceCreateDialog.amountPaid += ticket.total; this.$.balanceCreateDialog.amountPaid += ticket.totalWithVat;
this.$.balanceCreateDialog.clientFk = ticket.clientFk; this.$.balanceCreateDialog.clientFk = ticket.clientFk;
description.push(`${ticket.id}`); description.push(`${ticket.id}`);
} }
@ -109,7 +109,7 @@ export default class Controller extends Section {
} }
totalPriceColor(ticket) { totalPriceColor(ticket) {
const total = parseInt(ticket.total); const total = parseInt(ticket.totalWithVat);
if (total > 0 && total < 50) if (total > 0 && total < 50)
return 'warning'; return 'warning';
} }

View File

@ -6,18 +6,18 @@ describe('Component vnTicketIndex', () => {
let tickets = [{ let tickets = [{
id: 1, id: 1,
clientFk: 1, clientFk: 1,
total: 10.5, checked: false,
checked: false totalWithVat: 10.5
}, { }, {
id: 2, id: 2,
clientFk: 1, clientFk: 1,
total: 20.5, checked: true,
checked: true totalWithVat: 20.5
}, { }, {
id: 3, id: 3,
clientFk: 1, clientFk: 1,
total: 30, checked: true,
checked: true totalWithVat: 30
}]; }];
beforeEach(ngModule('ticket')); beforeEach(ngModule('ticket'));
@ -76,7 +76,6 @@ describe('Component vnTicketIndex', () => {
jest.spyOn(controller.$.balanceCreateDialog, 'show').mockReturnThis(); jest.spyOn(controller.$.balanceCreateDialog, 'show').mockReturnThis();
controller.$.model = {data: tickets}; controller.$.model = {data: tickets};
controller.$.balanceCreateDialog.amountPaid = 0;
controller.openBalanceDialog(); controller.openBalanceDialog();
let description = controller.$.balanceCreateDialog.description; let description = controller.$.balanceCreateDialog.description;

View File

@ -39,8 +39,8 @@ class Controller extends Section {
getSubTotal() { getSubTotal() {
if (!this.$params.id || !this.sales) return; if (!this.$params.id || !this.sales) return;
this.$http.get(`Tickets/${this.$params.id}/subtotal`).then(res => { this.$http.get(`Tickets/${this.$params.id}`).then(res => {
this.subtotal = res.data || 0.0; this.subtotal = res.data.totalWithoutVat || 0.0;
}); });
} }
@ -62,8 +62,8 @@ class Controller extends Section {
getVat() { getVat() {
this.VAT = 0.0; this.VAT = 0.0;
if (!this.$params.id || !this.sales) return; if (!this.$params.id || !this.sales) return;
this.$http.get(`Tickets/${this.$params.id}/getVAT`).then(res => { this.$http.get(`Tickets/${this.$params.id}`).then(res => {
this.VAT = res.data || 0.0; this.VAT = res.data.totalWithVat - res.data.totalWithoutVat || 0.0;
}); });
} }

View File

@ -84,13 +84,13 @@ describe('Ticket', () => {
describe('getSubTotal()', () => { describe('getSubTotal()', () => {
it('should make an HTTP GET query and then set the subtotal property', () => { it('should make an HTTP GET query and then set the subtotal property', () => {
const expectedAmount = 128; const expectedResponse = {totalWithoutVat: 128};
$httpBackend.expect('GET', 'Tickets/1/subtotal').respond(200, expectedAmount); $httpBackend.expect('GET', 'Tickets/1').respond(200, expectedResponse);
controller.getSubTotal(); controller.getSubTotal();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.subtotal).toEqual(expectedAmount); expect(controller.subtotal).toEqual(expectedResponse.totalWithoutVat);
}); });
}); });
@ -125,13 +125,15 @@ describe('Ticket', () => {
describe('getVat()', () => { describe('getVat()', () => {
it('should make an HTTP GET query and return the ticket VAT', () => { it('should make an HTTP GET query and return the ticket VAT', () => {
controller.edit = {}; controller.edit = {};
const expectedAmount = 67; const expectedResponse = {totalWithVat: 1000, totalWithoutVat: 999};
$httpBackend.expect('GET', 'Tickets/1/getVAT').respond(200, expectedAmount); const expectedVAT = expectedResponse.totalWithVat - expectedResponse.totalWithoutVat;
$httpBackend.expect('GET', 'Tickets/1').respond(200, expectedResponse);
controller.getVat(); controller.getVat();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.VAT).toEqual(expectedAmount); expect(controller.VAT).toEqual(expectedVAT);
}); });
}); });

View File

@ -99,9 +99,9 @@
</vn-label-value> </vn-label-value>
</vn-one> </vn-one>
<vn-one class="taxes"> <vn-one class="taxes">
<p><vn-label translate>Subtotal</vn-label> {{$ctrl.summary.subtotal | currency: 'EUR':2}}</p> <p><vn-label translate>Subtotal</vn-label> {{$ctrl.summary.totalWithoutVat | currency: 'EUR':2}}</p>
<p><vn-label translate>VAT</vn-label> {{$ctrl.summary.vat | currency: 'EUR':2}}</p> <p><vn-label translate>VAT</vn-label> {{$ctrl.summary.totalWithVat - $ctrl.summary.totalWithoutVat | currency: 'EUR':2}}</p>
<p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.summary.total | currency: 'EUR':2}}</strong></p> <p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.summary.totalWithVat | currency: 'EUR':2}}</strong></p>
</vn-one> </vn-one>
<vn-auto name="sales"> <vn-auto name="sales">
<h4> <h4>