Merge branch 'dev' into 2407-eliminar-trigger-ticketAfterUpdate
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
249ea918dd
|
@ -2,6 +2,12 @@ DELETE FROM `salix`.`ACL` WHERE id = 189;
|
|||
DELETE FROM `salix`.`ACL` WHERE id = 188;
|
||||
UPDATE `salix`.`ACL` tdms SET tdms.accessType = '*'
|
||||
WHERE tdms.id = 165;
|
||||
|
||||
INSERT INTO `salix`.`ACL` (model, principalId, property, accessType)
|
||||
VALUES
|
||||
('InvoiceInTax','administrative', '*', '*'),
|
||||
('InvoiceInLog','administrative', '*', 'READ');
|
||||
|
||||
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
|
||||
VALUES
|
||||
('InvoiceOut', 'createManualInvoice', 'WRITE', 'ALLOW', 'ROLE', 'invoicing'),
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Delete me!
|
|
@ -2340,13 +2340,20 @@ INSERT INTO `vn`.`duaInvoiceIn`(`id`, `duaFk`, `invoiceInFk`)
|
|||
(9, 9, 9),
|
||||
(10, 10, 10);
|
||||
|
||||
INSERT INTO `vn`.`invoiceInTax` (`invoiceInFk`, `taxCodeFk`, `taxableBase`, `expenceFk`, `foreignValue`, `taxTypeSageFk`, `transactionTypeSageFk`, `created`)
|
||||
INSERT INTO `vn`.`invoiceInTax` (`invoiceInFk`, `taxableBase`, `expenceFk`, `foreignValue`, `taxTypeSageFk`, `transactionTypeSageFk`)
|
||||
VALUES
|
||||
(1, 4, 99.99, '2000000000', null, null, null, CURDATE()),
|
||||
(2, 4, 999.99, '2000000000', null, null, null, CURDATE()),
|
||||
(3, 4, 1000.50, '2000000000', null, null, null, CURDATE()),
|
||||
(4, 4, 0.50, '2000000000', null, null, null, CURDATE()),
|
||||
(5, 4, 150.50, '2000000000', null, null, null, CURDATE());
|
||||
(1, 99.99, '2000000000', null, null, null),
|
||||
(2, 999.99, '2000000000', null, null, null),
|
||||
(3, 1000.50, '2000000000', null, null, null),
|
||||
(4, 0.50, '2000000000', null, null, null),
|
||||
(5, 150.50, '2000000000', null, null, null),
|
||||
(1, 252.25, '4751000000', NULL, 7, 61),
|
||||
(2, 223.17, '6210000567', NULL, 8, 20),
|
||||
(3, 95.60, '7001000000', NULL, 8, 35),
|
||||
(4, 446.63, '7001000000', NULL, 6, 61),
|
||||
(5, 64.23, '6210000567', NULL, 8, 20),
|
||||
(6, 29.95, '7001000000', NULL, 7, 20),
|
||||
(7, 58.64, '6210000567', NULL, 8, 20);
|
||||
|
||||
INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
|
||||
SELECT `id`
|
||||
|
|
|
@ -946,9 +946,11 @@ export default {
|
|||
ticketTwo: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(2)'
|
||||
},
|
||||
invoiceInSummary: {
|
||||
totalTaxableBase: 'vn-invoice-in-summary > vn-card > vn-horizontal > vn-auto > vn-horizontal > vn-one.taxes > span',
|
||||
supplierRef: 'vn-invoice-in-summary vn-label-value:nth-child(2) > section > span'
|
||||
},
|
||||
invoiceInDescriptor: {
|
||||
summaryIcon: 'vn-invoice-in-descriptor a[title="Preview"]',
|
||||
moreMenu: 'vn-invoice-in-descriptor vn-icon-button[icon=more_vert]',
|
||||
moreMenuDeleteInvoiceIn: '.vn-menu [name="deleteInvoice"]',
|
||||
moreMenuCloneInvoiceIn: '.vn-menu [name="cloneInvoice"]',
|
||||
|
@ -965,6 +967,17 @@ export default {
|
|||
company: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.companyFk"]',
|
||||
save: 'vn-invoice-in-basic-data button[type=submit]'
|
||||
},
|
||||
invoiceInTax: {
|
||||
addTaxButton: 'vn-invoice-in-tax vn-icon-button[icon="add_circle"]',
|
||||
thirdExpence: 'vn-invoice-in-tax vn-horizontal:nth-child(3) > vn-autocomplete[ng-model="invoiceInTax.expenseFk"]',
|
||||
thirdTaxableBase: 'vn-invoice-in-tax vn-horizontal:nth-child(3) > vn-input-number[ng-model="invoiceInTax.taxableBase"]',
|
||||
thirdTaxType: 'vn-invoice-in-tax vn-horizontal:nth-child(3) > vn-autocomplete[ng-model="invoiceInTax.taxTypeSageFk"]',
|
||||
thirdTransactionType: 'vn-invoice-in-tax vn-horizontal:nth-child(3) > vn-autocomplete[ng-model="invoiceInTax.transactionTypeSageFk"]',
|
||||
thirdRate: 'vn-invoice-in-tax > form > vn-card > vn-horizontal:nth-child(3) > vn-textfield',
|
||||
thirdDeleteButton: 'vn-invoice-in-tax vn-horizontal:nth-child(3) vn-icon[icon="delete"]',
|
||||
saveButton: 'vn-invoice-in-tax vn-submit',
|
||||
|
||||
},
|
||||
travelIndex: {
|
||||
anySearchResult: 'vn-travel-index vn-tbody > a',
|
||||
firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)',
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('InvoiceIn basic data path', () => {
|
|||
await page.write(selectors.invoiceInBasicData.supplierRef, '9999');
|
||||
await page.pickDate(selectors.invoiceInBasicData.bookEntried, now);
|
||||
await page.pickDate(selectors.invoiceInBasicData.booked, now);
|
||||
await page.autocompleteSearch(selectors.invoiceInBasicData.currency, 'Dollar USA');
|
||||
await page.autocompleteSearch(selectors.invoiceInBasicData.currency, 'USD');
|
||||
await page.autocompleteSearch(selectors.invoiceInBasicData.company, 'ORN');
|
||||
await page.waitToClick(selectors.invoiceInBasicData.save);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
@ -52,7 +52,7 @@ describe('InvoiceIn basic data path', () => {
|
|||
const result = await page
|
||||
.waitToGetProperty(selectors.invoiceInBasicData.currency, 'value');
|
||||
|
||||
expect(result).toEqual('Dollar USA');
|
||||
expect(result).toEqual('USD');
|
||||
});
|
||||
|
||||
it(`should confirm the invoiceIn company was edited`, async() => {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import selectors from '../../helpers/selectors.js';
|
||||
import getBrowser from '../../helpers/puppeteer';
|
||||
|
||||
describe('InvoiceIn tax path', () => {
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('developer', 'invoiceIn');
|
||||
await page.accessToSearchResult('2');
|
||||
await page.accessToSection('invoiceIn.card.tax');
|
||||
});
|
||||
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it('should add a new tax', async() => {
|
||||
await page.waitToClick(selectors.invoiceInTax.addTaxButton);
|
||||
await page.autocompleteSearch(selectors.invoiceInTax.thirdExpence, '6210000567');
|
||||
await page.write(selectors.invoiceInTax.thirdTaxableBase, '100');
|
||||
await page.autocompleteSearch(selectors.invoiceInTax.thirdTaxType, '6');
|
||||
await page.autocompleteSearch(selectors.invoiceInTax.thirdTransactionType, 'Operaciones exentas');
|
||||
await page.waitToClick(selectors.invoiceInTax.saveButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('Data saved!');
|
||||
});
|
||||
|
||||
it('should navigate to the summary and check the taxable base sum is correct', async() => {
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.summaryIcon);
|
||||
await page.waitForState('invoiceIn.card.summary');
|
||||
const result = await page.waitToGetProperty(selectors.invoiceInSummary.totalTaxableBase, 'innerText');
|
||||
|
||||
expect(result).toEqual('Taxable base €1,323.16');
|
||||
});
|
||||
|
||||
it('should navigate back to the tax section and check the reciently added line contains the expected expense', async() => {
|
||||
await page.accessToSection('invoiceIn.card.tax');
|
||||
const result = await page.waitToGetProperty(selectors.invoiceInTax.thirdExpence, 'value');
|
||||
|
||||
expect(result).toEqual('6210000567: Alquiler VNH');
|
||||
});
|
||||
|
||||
it('should check the reciently added line contains the expected taxable base', async() => {
|
||||
const result = await page.waitToGetProperty(selectors.invoiceInTax.thirdTaxableBase, 'value');
|
||||
|
||||
expect(result).toEqual('100');
|
||||
});
|
||||
|
||||
it('should check the reciently added line contains the expected tax type', async() => {
|
||||
const result = await page.waitToGetProperty(selectors.invoiceInTax.thirdTaxType, 'value');
|
||||
|
||||
expect(result).toEqual('6: H.P. IVA 4% CEE');
|
||||
});
|
||||
|
||||
it('should check the reciently added line contains the expected transaction type', async() => {
|
||||
const result = await page.waitToGetProperty(selectors.invoiceInTax.thirdTransactionType, 'value');
|
||||
|
||||
expect(result).toEqual('37: Operaciones exentas');
|
||||
});
|
||||
|
||||
it('should check the reciently added line contains the expected rate', async() => {
|
||||
const result = await page.waitToGetProperty(selectors.invoiceInTax.thirdRate, 'value');
|
||||
|
||||
expect(result).toEqual('€4.00');
|
||||
});
|
||||
|
||||
it('should delete the added line', async() => {
|
||||
await page.waitToClick(selectors.invoiceInTax.thirdDeleteButton);
|
||||
await page.waitToClick(selectors.invoiceInTax.saveButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('Data saved!');
|
||||
});
|
||||
});
|
|
@ -113,5 +113,6 @@
|
|||
"reserved": "reserved",
|
||||
"Global invoicing failed": "[Global invoicing] Wasn't able to invoice some of the clients",
|
||||
"A ticket with a negative base can't be invoiced": "A ticket with a negative base can't be invoiced",
|
||||
"This client is not invoiceable": "This client is not invoiceable"
|
||||
"This client is not invoiceable": "This client is not invoiceable",
|
||||
"INACTIVE_PROVIDER": "Inactive provider"
|
||||
}
|
|
@ -195,6 +195,16 @@
|
|||
"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",
|
||||
"INACTIVE_PROVIDER": "INACTIVE_PROVIDER",
|
||||
"nickname": "nickname"
|
||||
"nickname": "nickname",
|
||||
"INACTIVE_PROVIDER": "Proveedor inactivo",
|
||||
"This client is not invoiceable": "Este cliente no es facturable",
|
||||
"serial non editable": "Esta serie no permite asignar la referencia",
|
||||
"Max shipped required": "La fecha límite es requerida",
|
||||
"Can't invoice to future": "No se puede facturar a futuro",
|
||||
"Can't invoice to past": "No se puede facturar a pasado",
|
||||
"This ticket is already invoiced": "Este ticket ya está facturado",
|
||||
"A ticket with an amount of zero can't be invoiced": "No se puede facturar un ticket con importe cero",
|
||||
"A ticket with a negative base can't be invoiced": "No se puede facturar un ticket con una base negativa",
|
||||
"Global invoicing failed": "[Facturación global] No se han podido facturar algunos clientes",
|
||||
"Wasn't able to invoice the following clients": "No se han podido facturar los siguientes clientes"
|
||||
}
|
|
@ -20,6 +20,12 @@
|
|||
"mysql": {
|
||||
"columnName": "Iva"
|
||||
}
|
||||
},
|
||||
"rate": {
|
||||
"type": "number",
|
||||
"mysql": {
|
||||
"columnName": "PorcentajeIva"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<vn-crud-model
|
||||
vn-id="ticketsModel"
|
||||
auto-load="true"
|
||||
url="Tickets"
|
||||
link="{clientFk: $ctrl.$params.id}"
|
||||
link="{clientFk: $ctrl.client.id}"
|
||||
filter="::$ctrl.ticketFilter"
|
||||
limit="5"
|
||||
data="tickets"
|
||||
|
@ -297,8 +296,8 @@
|
|||
<vn-th field="routeFk" expand>Route</vn-th>
|
||||
<vn-th field="packages" shrink>Packages</vn-th>
|
||||
<vn-th field="shipped" shrink-date>Date</vn-th>
|
||||
<vn-th>State</vn-th>
|
||||
<vn-th shrink>Total</vn-th>
|
||||
<vn-th field="stateFk">State</vn-th>
|
||||
<vn-th field="totalWithVat" shrink>Total</vn-th>
|
||||
<vn-th></vn-th>
|
||||
</vn-tr>
|
||||
</vn-thead>
|
||||
|
|
|
@ -33,10 +33,19 @@ class Controller extends Summary {
|
|||
};
|
||||
}
|
||||
|
||||
$onChanges() {
|
||||
if (!this.client)
|
||||
return;
|
||||
get client() {
|
||||
return this._client;
|
||||
}
|
||||
|
||||
set client(value) {
|
||||
this._client = value;
|
||||
if (value) {
|
||||
this.loadData();
|
||||
this.loadTickets();
|
||||
}
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.$http.get(`Clients/${this.client.id}/summary`).then(res => {
|
||||
if (res && res.data) {
|
||||
this.summary = res.data;
|
||||
|
@ -49,6 +58,10 @@ class Controller extends Summary {
|
|||
});
|
||||
}
|
||||
|
||||
loadTickets() {
|
||||
this.$.$applyAsync(() => this.$.ticketsModel.refresh());
|
||||
}
|
||||
|
||||
get isEmployee() {
|
||||
return this.aclService.hasAny(['employee']);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import './index';
|
||||
import crudModel from 'core/mocks/crud-model';
|
||||
|
||||
describe('Client', () => {
|
||||
describe('Component vnClientSummary', () => {
|
||||
|
@ -13,17 +14,30 @@ describe('Client', () => {
|
|||
$httpBackend = _$httpBackend_;
|
||||
const $element = angular.element('<vn-client-summary></vn-client-summary>');
|
||||
controller = $componentController('vnClientSummary', {$element});
|
||||
controller.client = {id: 1101};
|
||||
controller._client = {id: 1101};
|
||||
controller.$.ticketsModel = crudModel;
|
||||
}));
|
||||
|
||||
describe('$onChanges()', () => {
|
||||
describe('client() setter', () => {
|
||||
it('should call to the loadData() and loadTickets() methods', () => {
|
||||
controller.loadData = jest.fn();
|
||||
controller.loadTickets = jest.fn();
|
||||
|
||||
controller.client = {id: 1102};
|
||||
|
||||
expect(controller.loadData).toHaveBeenCalledWith();
|
||||
expect(controller.loadTickets).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadData()', () => {
|
||||
it('should perform a GET query and then define the summary property', () => {
|
||||
let res = {name: 'Superman', classifications: []};
|
||||
|
||||
jest.spyOn(controller, 'sumRisk').mockReturnThis();
|
||||
$httpBackend.expect('GET', `Clients/1101/summary`).respond(200, res);
|
||||
|
||||
controller.$onChanges();
|
||||
controller.loadData();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.summary).toBeDefined();
|
||||
|
@ -31,6 +45,17 @@ describe('Client', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('loadTickets()', () => {
|
||||
it('should call to the model refresh() method', () => {
|
||||
jest.spyOn(controller.$.ticketsModel, 'refresh');
|
||||
|
||||
controller.loadTickets();
|
||||
controller.$.$apply();
|
||||
|
||||
expect(controller.$.ticketsModel.refresh).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sumRisk()', () => {
|
||||
it('should sum property amount of an array', () => {
|
||||
controller.summary = {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
Buys: Compras
|
||||
Delete buy(s): Eliminar compra(s)
|
||||
Add buy: Añadir compra
|
|
@ -69,25 +69,13 @@ module.exports = Self => {
|
|||
deductibleExpenseFk: sourceInvoiceIn.deductibleExpenseFk,
|
||||
}, myOptions);
|
||||
|
||||
const oldProperties = await loggable.translateValues(Self, sourceInvoiceIn);
|
||||
const newProperties = await loggable.translateValues(Self, clone);
|
||||
await models.InvoiceInLog.create({
|
||||
originFk: clone.id,
|
||||
userFk: userId,
|
||||
action: 'insert',
|
||||
changedModel: 'InvoiceIn',
|
||||
changedModelId: clone.id,
|
||||
oldInstance: oldProperties,
|
||||
newInstance: newProperties
|
||||
}, myOptions);
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (let tax of sourceInvoiceInTax) {
|
||||
promises.push(models.InvoiceInTax.create({
|
||||
invoiceInFk: clone.id,
|
||||
taxableBase: tax.taxableBase,
|
||||
expenceFk: tax.expenceFk,
|
||||
expenseFk: tax.expenseFk,
|
||||
foreignValue: tax.foreignValue,
|
||||
taxTypeSageFk: tax.taxTypeSageFk,
|
||||
transactionTypeSageFk: tax.transactionTypeSageFk
|
||||
|
|
|
@ -21,7 +21,7 @@ describe('invoiceIn clone()', () => {
|
|||
|
||||
const invoiceInTaxes = await models.InvoiceInTax.find({where: {invoiceInFk: clone.id}}, options);
|
||||
|
||||
expect(invoiceInTaxes.length).toEqual(1);
|
||||
expect(invoiceInTaxes.length).toEqual(2);
|
||||
|
||||
const invoiceInDueDays = await models.InvoiceInDueDay.find({where: {invoiceInFk: clone.id}}, options);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ describe('invoiceIn summary()', () => {
|
|||
const summary = await models.InvoiceIn.summary(1, options);
|
||||
|
||||
expect(summary.supplierRef).toEqual('1234');
|
||||
expect(summary.totals.totalTaxableBase).toEqual(352.24);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
|
|
|
@ -45,6 +45,31 @@ module.exports = Self => {
|
|||
fields: ['withholding']
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'invoiceInTax',
|
||||
scope: {
|
||||
fields: [
|
||||
'id',
|
||||
'invoiceInFk',
|
||||
'taxableBase',
|
||||
'expenseFk',
|
||||
'taxTypeSageFk',
|
||||
'transactionTypeSageFk',
|
||||
'foreignValue'],
|
||||
include: [{
|
||||
relation: 'transactionTypeSage',
|
||||
scope: {
|
||||
fields: ['transaction']
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'taxTypeSage',
|
||||
scope: {
|
||||
fields: ['vat']
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'expenseDeductible',
|
||||
scope: {
|
||||
|
@ -54,12 +79,31 @@ module.exports = Self => {
|
|||
{
|
||||
relation: 'currency',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
fields: ['id', 'code']
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return Self.app.models.InvoiceIn.findById(id, filter, myOptions);
|
||||
let summaryObj = await Self.app.models.InvoiceIn.findById(id, filter, myOptions);
|
||||
|
||||
summaryObj.totals = await getTotals(id);
|
||||
return summaryObj;
|
||||
};
|
||||
|
||||
async function getTotals(invoiceInFk) {
|
||||
return (await Self.rawSql(`
|
||||
SELECT iit.*,
|
||||
SUM(iidd.amount) totalDueDay
|
||||
FROM vn.invoiceIn ii
|
||||
LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase,
|
||||
SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) totalVat
|
||||
FROM vn.invoiceInTax iit
|
||||
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
|
||||
WHERE iit.invoiceInFk = ?) iit ON TRUE
|
||||
LEFT JOIN vn.invoiceInDueDay iidd ON iidd.invoiceInFk = ii.id
|
||||
WHERE
|
||||
ii.id = ?`, [invoiceInFk, invoiceInFk]))[0];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"name": "InvoiceInTax",
|
||||
"base": "VnModel",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "InvoiceInLog",
|
||||
"relation": "invoiceIn"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "invoiceInTax"
|
||||
|
@ -12,31 +16,43 @@
|
|||
"type": "number",
|
||||
"description": "Identifier"
|
||||
},
|
||||
"invoiceInFk": {
|
||||
"type": "number"
|
||||
},
|
||||
"taxCodeFk": {
|
||||
"type": "number"
|
||||
},
|
||||
"taxableBase": {
|
||||
"type": "number"
|
||||
},
|
||||
"expenceFk": {
|
||||
"type": "string"
|
||||
},
|
||||
"foreignValue": {
|
||||
"type": "number"
|
||||
},
|
||||
"taxTypeSageFk": {
|
||||
"type": "number"
|
||||
},
|
||||
"transactionTypeSageFk": {
|
||||
"type": "number"
|
||||
"expenseFk": {
|
||||
"type": "number",
|
||||
"mysql": {
|
||||
"columnName": "expenceFk"
|
||||
}
|
||||
},
|
||||
"created": {
|
||||
"type": "date"
|
||||
}
|
||||
|
||||
},
|
||||
"relations": {
|
||||
"invoiceIn": {
|
||||
"type": "belongsTo",
|
||||
"model": "InvoiceIn",
|
||||
"foreignKey": "invoiceInFk"
|
||||
},
|
||||
"expense": {
|
||||
"type": "belongsTo",
|
||||
"model": "Expense",
|
||||
"foreignKey": "expenseFk"
|
||||
},
|
||||
"taxTypeSage": {
|
||||
"type": "belongsTo",
|
||||
"model": "SageTaxType",
|
||||
"foreignKey": "taxTypeSageFk"
|
||||
},
|
||||
"transactionTypeSage": {
|
||||
"type": "belongsTo",
|
||||
"model": "SageTransactionType",
|
||||
"foreignKey": "transactionTypeSageFk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -64,6 +64,11 @@
|
|||
"model": "InvoiceInDueDay",
|
||||
"foreignKey": "invoiceInFk"
|
||||
},
|
||||
"invoiceInTax": {
|
||||
"type": "hasMany",
|
||||
"model": "InvoiceInTax",
|
||||
"foreignKey": "invoiceInFk"
|
||||
},
|
||||
"sageWithholding": {
|
||||
"type": "belongsTo",
|
||||
"model": "SageWithholding",
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
label="Currency"
|
||||
ng-model="$ctrl.invoiceIn.currencyFk"
|
||||
url="Currencies"
|
||||
show-field="name"
|
||||
show-field="code"
|
||||
value-field="id"
|
||||
rule>
|
||||
</vn-autocomplete>
|
||||
|
|
|
@ -13,6 +13,9 @@ class Controller extends ModuleCard {
|
|||
},
|
||||
{
|
||||
relation: 'company'
|
||||
},
|
||||
{
|
||||
relation: 'currency'
|
||||
}
|
||||
]};
|
||||
|
||||
|
|
|
@ -8,5 +8,6 @@ import './descriptor';
|
|||
import './descriptor-popover';
|
||||
import './summary';
|
||||
import './basic-data';
|
||||
import './tax';
|
||||
import './create';
|
||||
import './log';
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
InvoiceIn: Facturas recibidas
|
||||
Search invoices in by reference: Buscar facturas recibidas por referencia
|
||||
Entries list: Listado de entradas
|
||||
Invoice list: Listado de facturas recibidas
|
||||
InvoiceIn deleted: Factura eliminada
|
||||
Remove tax: Quitar iva
|
||||
Add tax: Añadir iva
|
||||
Sage tax: Sage iva
|
||||
Sage transaction: Sage transaccion
|
||||
Foreign value: Divisa
|
||||
Due day: Vencimiento
|
||||
Invoice list: Listado de facturas recibidas
|
||||
InvoiceIn cloned: Factura clonada
|
||||
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
"state": "invoiceIn.card.basicData",
|
||||
"icon": "settings"
|
||||
},
|
||||
{
|
||||
"state": "invoiceIn.card.tax",
|
||||
"icon": "icon-lines"
|
||||
},
|
||||
{
|
||||
"state": "invoiceIn.card.log",
|
||||
"icon": "history"
|
||||
|
@ -81,6 +85,16 @@
|
|||
"administrative"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "/tax",
|
||||
"state": "invoiceIn.card.tax",
|
||||
"component": "vn-invoice-in-tax",
|
||||
"description": "Tax",
|
||||
"params": {
|
||||
"invoice-in": "$ctrl.invoiceIn"
|
||||
},
|
||||
"acl": ["administrative"]
|
||||
},
|
||||
{
|
||||
"url": "/log",
|
||||
"state": "invoiceIn.card.log",
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</vn-label-value>
|
||||
<vn-label-value label="Supplier ref" value="{{$ctrl.summary.supplierRef}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Currency" value="{{$ctrl.summary.currency.name}}">
|
||||
<vn-label-value label="Currency" value="{{$ctrl.summary.currency.code}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Doc number" value="{{$ctrl.summary.serial}}/{{$ctrl.summary.serialNumber}}">
|
||||
</vn-label-value>
|
||||
|
@ -45,8 +45,51 @@
|
|||
</vn-check>
|
||||
</vn-vertical>
|
||||
</vn-one>
|
||||
<vn-one tbody class="taxes">
|
||||
<span td class="chip"><vn-label translate>Taxable base</vn-label> {{$ctrl.summary.totals.totalTaxableBase | currency: 'EUR':2 | dashIfEmpty}}</span>
|
||||
<p><vn-label translate>Total</vn-label> {{$ctrl.summary.totals.totalVat | currency: 'EUR':2 | dashIfEmpty}}</p>
|
||||
<vn-label translate>Due day</vn-label>
|
||||
<vn-chip
|
||||
class="transparent"
|
||||
ng-class="{'alert': $ctrl.amountsNotMatch}"
|
||||
translate-attr="{title: $ctrl.amountsNotMatch ? 'Do not match' : 'Due day'}"
|
||||
>{{$ctrl.summary.totals.totalDueDay | currency: 'EUR':2 | dashIfEmpty}}
|
||||
</vn-chip>
|
||||
</vn-one>
|
||||
</vn-horizontal>
|
||||
</vn-auto>
|
||||
|
||||
<vn-one ng-if="$ctrl.summary.invoiceInTax.length != 0">
|
||||
<h4>
|
||||
<a
|
||||
ui-sref="invoiceIn.card.tax({id:$ctrl.invoiceIn.id})"
|
||||
target="_self">
|
||||
<span translate vn-tooltip="Go to">Vat</span>
|
||||
</a>
|
||||
</h4>
|
||||
<vn-table model="model">
|
||||
<vn-thead>
|
||||
<vn-tr>
|
||||
<vn-th number>Expense</vn-th>
|
||||
<vn-th shrink>Taxable base</vn-th>
|
||||
<vn-th>Sage vat</vn-th>
|
||||
<vn-th>Sage transaction</vn-th>
|
||||
<vn-th number shrink>Rate</vn-th>
|
||||
<vn-th number shrink>Foreign value</vn-th>
|
||||
</vn-tr>
|
||||
</vn-thead>
|
||||
<vn-tbody>
|
||||
<vn-tr ng-repeat="tax in $ctrl.summary.invoiceInTax">
|
||||
<vn-td number expand>{{::tax.expenseFk}}</vn-td>
|
||||
<vn-td shrink>{{::tax.taxableBase | currency: 'EUR':2}}</vn-td>
|
||||
<vn-td>{{::tax.taxTypeSage.vat}}</vn-td>
|
||||
<vn-td>{{::tax.transactionTypeSage.transaction}}</vn-td>
|
||||
<vn-td number shrink>{{::tax.taxRate | percentage}}</vn-td>
|
||||
<vn-td number shrink>{{::tax.foreignValue | currency: 'USD':2}}</vn-td>
|
||||
</vn-tr>
|
||||
</vn-tbody>
|
||||
</vn-table>
|
||||
</vn-one>
|
||||
</vn-horizontal>
|
||||
</vn-card>
|
||||
<vn-supplier-descriptor-popover
|
||||
|
|
|
@ -17,6 +17,13 @@ class Controller extends Summary {
|
|||
return this.$http.get(`InvoiceIns/${this.invoiceIn.id}/summary`)
|
||||
.then(res => this.summary = res.data);
|
||||
}
|
||||
|
||||
get amountsNotMatch() {
|
||||
if (!this.summary) return false;
|
||||
|
||||
const total = this.summary.totals;
|
||||
return total.totalDueDay != total.totalTaxableBase && total.totalDueDay != total.totalVat;
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnInvoiceInSummary', {
|
||||
|
|
|
@ -25,5 +25,34 @@ describe('InvoiceIn', () => {
|
|||
expect(controller.summary).toEqual('the data you are looking for');
|
||||
});
|
||||
});
|
||||
|
||||
describe('amountsNotMatch getter()', () => {
|
||||
it('should get false when taxamount match with due day amount', () => {
|
||||
controller.summary =
|
||||
{
|
||||
totals: {
|
||||
totalDueDay: 'amount match',
|
||||
totalTaxableBase: 'amount match',
|
||||
totalVat: 'no care'
|
||||
}
|
||||
};
|
||||
|
||||
expect(controller.amountsNotMatch).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should get true when taxamount does not match with due day amount', () => {
|
||||
controller.summary =
|
||||
{
|
||||
totals: {
|
||||
totalDueDay: 'amount does not match',
|
||||
totalTaxableBase: 'neither match',
|
||||
totalVat: 'no care'
|
||||
}
|
||||
};
|
||||
console.log(controller);
|
||||
|
||||
expect(controller.amountsNotMatch).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,3 +8,4 @@ Accounted date: Fecha contable
|
|||
Doc number: Numero documento
|
||||
Sage withholding: Retención sage
|
||||
Undeductible VAT: Iva no deducible
|
||||
Do not match: No coinciden
|
|
@ -1,5 +1,13 @@
|
|||
@import "variables";
|
||||
@import "./variables";
|
||||
|
||||
vn-invoice-in-summary .summary {
|
||||
width: $width-lg;
|
||||
max-width: $width-xl;
|
||||
|
||||
.taxes {
|
||||
border: $border-thin-light;
|
||||
text-align: right;
|
||||
padding: 8px;
|
||||
font-size: 1.2rem;
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
<vn-crud-model
|
||||
vn-id="model"
|
||||
url="InvoiceInTaxes"
|
||||
fields="[
|
||||
'id',
|
||||
'invoiceInFk',
|
||||
'taxableBase',
|
||||
'expenseFk',
|
||||
'foreignValue',
|
||||
'taxTypeSageFk',
|
||||
'transactionTypeSageFk']"
|
||||
link="{invoiceInFk: $ctrl.$params.id}"
|
||||
data="$ctrl.taxes"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<vn-crud-model
|
||||
url="Expenses"
|
||||
data="expenses"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<vn-watcher
|
||||
vn-id="watcher"
|
||||
data="$ctrl.taxes"
|
||||
form="form">
|
||||
</vn-watcher>
|
||||
<form name="form" ng-submit="$ctrl.onSubmit()">
|
||||
<vn-card class="vn-pa-lg">
|
||||
<vn-horizontal ng-repeat="invoiceInTax in $ctrl.taxes">
|
||||
<vn-autocomplete vn-three vn-id="expense" vn-focus
|
||||
label="Expense"
|
||||
ng-model="invoiceInTax.expenseFk"
|
||||
data="expenses"
|
||||
show-field="id"
|
||||
rule>
|
||||
<tpl-item>{{id}}: {{name}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
<vn-input-number vn-one
|
||||
disabled="$ctrl.invoiceIn.currency.code != 'EUR'"
|
||||
step="0.01"
|
||||
label="Taxable base"
|
||||
ng-model="invoiceInTax.taxableBase"
|
||||
rule>
|
||||
</vn-input-number>
|
||||
<vn-autocomplete vn-three
|
||||
label="Sage tax"
|
||||
ng-model="invoiceInTax.taxTypeSageFk"
|
||||
url="SageTaxTypes"
|
||||
show-field="vat"
|
||||
fields="['id', 'vat', 'rate']"
|
||||
search-function="{or: [{id: $search}, {vat: {like: '%'+ $search +'%'}}]}"
|
||||
selection="taxRateSelection"
|
||||
rule>
|
||||
<tpl-item>{{id}}: {{vat}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete vn-three
|
||||
label="Sage transaction"
|
||||
ng-model="invoiceInTax.transactionTypeSageFk"
|
||||
url="SageTransactionTypes"
|
||||
show-field="transaction"
|
||||
search-function="{or: [{id: $search}, {transaction: {like: '%'+ $search +'%'}}]}"
|
||||
rule>
|
||||
<tpl-item>{{id}}: {{transaction}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
<vn-textfield
|
||||
disabled="true"
|
||||
label="Rate"
|
||||
field="$ctrl.taxRate(invoiceInTax, taxRateSelection) | currency:'EUR':2">
|
||||
</vn-textfield>
|
||||
<vn-input-number
|
||||
disabled="$ctrl.invoiceIn.currency.code == 'EUR'"
|
||||
label="Foreign value"
|
||||
ng-model="invoiceInTax.foreignValue"
|
||||
rule>
|
||||
</vn-input-number>
|
||||
<vn-none>
|
||||
<vn-icon-button
|
||||
vn-tooltip="Remove tax"
|
||||
icon="delete"
|
||||
ng-click="model.remove($index)"
|
||||
tabindex="-1">
|
||||
</vn-icon-button>
|
||||
</vn-none>
|
||||
</vn-horizontal>
|
||||
<vn-one>
|
||||
<vn-icon-button
|
||||
vn-bind="+"
|
||||
vn-tooltip="Add tax"
|
||||
icon="add_circle"
|
||||
ng-click="$ctrl.add()">
|
||||
</vn-icon-button>
|
||||
</vn-one>
|
||||
</vn-card>
|
||||
<vn-button-bar>
|
||||
<vn-submit
|
||||
disabled="!watcher.dataChanged()"
|
||||
label="Save">
|
||||
</vn-submit>
|
||||
</vn-button-bar>
|
||||
</form>
|
|
@ -0,0 +1,40 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
class Controller extends Section {
|
||||
taxRate(invoiceInTax, taxRateSelection) {
|
||||
const taxTypeSage = taxRateSelection && taxRateSelection.rate;
|
||||
const taxableBase = invoiceInTax && invoiceInTax.taxableBase;
|
||||
|
||||
if (taxTypeSage && taxableBase)
|
||||
return (taxTypeSage / 100) * taxableBase;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
add() {
|
||||
this.$.model.insert({
|
||||
invoiceIn: this.$params.id
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.$.watcher.check();
|
||||
this.$.model.save().then(() => {
|
||||
this.$.watcher.notifySaved();
|
||||
this.$.watcher.updateOriginalData();
|
||||
this.card.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnInvoiceInTax', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller,
|
||||
require: {
|
||||
card: '^vnInvoiceInCard'
|
||||
},
|
||||
bindings: {
|
||||
invoiceIn: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
import './index.js';
|
||||
import watcher from 'core/mocks/watcher';
|
||||
import crudModel from 'core/mocks/crud-model';
|
||||
|
||||
describe('InvoiceIn', () => {
|
||||
describe('Component tax', () => {
|
||||
let controller;
|
||||
let $scope;
|
||||
let vnApp;
|
||||
|
||||
beforeEach(ngModule('invoiceIn'));
|
||||
|
||||
beforeEach(inject(($componentController, _$httpBackend_, $rootScope, _vnApp_) => {
|
||||
vnApp = _vnApp_;
|
||||
jest.spyOn(vnApp, 'showError');
|
||||
$scope = $rootScope.$new();
|
||||
$scope.model = crudModel;
|
||||
|
||||
$scope.watcher = watcher;
|
||||
const $element = angular.element('<vn-invoice-in-tax></vn-invoice-in-tax>');
|
||||
controller = $componentController('vnInvoiceInTax', {$element, $scope});
|
||||
controller.invoiceIn = {id: 1};
|
||||
}));
|
||||
|
||||
describe('taxRate()', () => {
|
||||
it('should set tax rate with the Sage tax type value', () => {
|
||||
const taxRateSelection = {
|
||||
rate: 21
|
||||
};
|
||||
const invoiceInTax = {
|
||||
taxableBase: 200
|
||||
};
|
||||
|
||||
const taxRate = controller.taxRate(invoiceInTax, taxRateSelection);
|
||||
|
||||
expect(taxRate).toEqual(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSubmit()', () => {
|
||||
it('should make HTTP POST request to save tax values', () => {
|
||||
controller.card = {reload: () => {}};
|
||||
jest.spyOn($scope.watcher, 'check');
|
||||
jest.spyOn($scope.watcher, 'notifySaved');
|
||||
jest.spyOn($scope.watcher, 'updateOriginalData');
|
||||
jest.spyOn(controller.card, 'reload');
|
||||
jest.spyOn($scope.model, 'save');
|
||||
|
||||
controller.onSubmit();
|
||||
|
||||
expect($scope.model.save).toHaveBeenCalledWith();
|
||||
expect($scope.watcher.updateOriginalData).toHaveBeenCalledWith();
|
||||
expect($scope.watcher.check).toHaveBeenCalledWith();
|
||||
expect($scope.watcher.notifySaved).toHaveBeenCalledWith();
|
||||
expect(controller.card.reload).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
<vn-crud-model
|
||||
vn-id="ticketsModel"
|
||||
url="InvoiceOuts/{{$ctrl.invoiceOut.id}}/getTickets"
|
||||
limit="10"
|
||||
auto-load="true"
|
||||
data="tickets">
|
||||
</vn-crud-model>
|
||||
<vn-card class="summary">
|
||||
|
|
|
@ -3,22 +3,26 @@ import Summary from 'salix/components/summary';
|
|||
import './style.scss';
|
||||
|
||||
class Controller extends Summary {
|
||||
set invoiceOut(value) {
|
||||
this._invoiceOut = value;
|
||||
if (value && value.id) {
|
||||
this.getSummary();
|
||||
this.$.ticketsModel.url = `InvoiceOuts/${this.invoiceOut.id}/getTickets`;
|
||||
}
|
||||
}
|
||||
|
||||
get invoiceOut() {
|
||||
return this._invoiceOut;
|
||||
}
|
||||
|
||||
getSummary() {
|
||||
return this.$http.get(`InvoiceOuts/${this.invoiceOut.id}/summary`)
|
||||
set invoiceOut(value) {
|
||||
this._invoiceOut = value;
|
||||
if (value && value.id) {
|
||||
this.loadData();
|
||||
this.loadTickets();
|
||||
}
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.$http.get(`InvoiceOuts/${this.invoiceOut.id}/summary`)
|
||||
.then(res => this.summary = res.data);
|
||||
}
|
||||
|
||||
loadTickets() {
|
||||
this.$.$applyAsync(() => this.$.ticketsModel.refresh());
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnInvoiceOutSummary', {
|
||||
|
|
|
@ -18,10 +18,10 @@ describe('InvoiceOut', () => {
|
|||
controller.$.ticketsModel = crudModel;
|
||||
}));
|
||||
|
||||
describe('getSummary()', () => {
|
||||
describe('loadData()', () => {
|
||||
it('should perform a query to set summary', () => {
|
||||
$httpBackend.expect('GET', `InvoiceOuts/1/summary`).respond(200, 'the data you are looking for');
|
||||
controller.getSummary();
|
||||
controller.loadData();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.summary).toEqual('the data you are looking for');
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<vn-th field="provinceFk" class="expendable">Province</vn-th>
|
||||
<vn-th field="stateFk">State</vn-th>
|
||||
<vn-th field="zoneFk">Zone</vn-th>
|
||||
<vn-th shrink>Total</vn-th>
|
||||
<vn-th field="totalWithVat" shrink>Total</vn-th>
|
||||
<vn-th></vn-th>
|
||||
</vn-tr>
|
||||
</vn-thead>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -42,6 +42,7 @@
|
|||
"strong-error-handler": "^2.3.2",
|
||||
"uuid": "^3.3.3",
|
||||
"vn-loopback": "file:./loopback",
|
||||
"vn-mysql": "^1.0.3",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
Loading…
Reference in New Issue