Merge branch 'dev' into 2407-eliminar-trigger-ticketAfterUpdate
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2021-08-13 08:18:34 +02:00
commit 249ea918dd
38 changed files with 3234 additions and 4869 deletions

View File

@ -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'),

View File

@ -0,0 +1 @@
Delete me!

View File

@ -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`

View File

@ -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)',

View File

@ -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() => {

View File

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

View File

@ -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"
}

View File

@ -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"
}

View File

@ -20,6 +20,12 @@
"mysql": {
"columnName": "Iva"
}
},
"rate": {
"type": "number",
"mysql": {
"columnName": "PorcentajeIva"
}
}
},
"acls": [

View File

@ -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>

View File

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

View File

@ -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 = {

View File

@ -1,2 +1,3 @@
Buys: Compras
Delete buy(s): Eliminar compra(s)
Add buy: Añadir compra

View File

@ -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

View File

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

View File

@ -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) {

View File

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

View File

@ -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"
}
}
}

View File

@ -64,6 +64,11 @@
"model": "InvoiceInDueDay",
"foreignKey": "invoiceInFk"
},
"invoiceInTax": {
"type": "hasMany",
"model": "InvoiceInTax",
"foreignKey": "invoiceInFk"
},
"sageWithholding": {
"type": "belongsTo",
"model": "SageWithholding",

View File

@ -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>

View File

@ -13,6 +13,9 @@ class Controller extends ModuleCard {
},
{
relation: 'company'
},
{
relation: 'currency'
}
]};

View File

@ -8,5 +8,6 @@ import './descriptor';
import './descriptor-popover';
import './summary';
import './basic-data';
import './tax';
import './create';
import './log';

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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', {

View File

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

View File

@ -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

View File

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

View File

@ -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>

View File

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

View File

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

View File

@ -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">

View File

@ -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', {

View File

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

View File

@ -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>

7419
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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": {