Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 3951-invoiceOut.index_downloadPdfs
gitea/salix/pipeline/head This commit is unstable Details

This commit is contained in:
Vicent Llopis 2022-11-03 10:17:46 +01:00
commit b6d3a08871
76 changed files with 1633 additions and 512 deletions

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceIn', 'invoiceInPdf', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', 'invoiceInEmail', 'WRITE', 'ALLOW', 'ROLE', 'administrative'),

View File

@ -1,37 +0,0 @@
const app = require('vn-loopback/server/server');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
// #1885
xdescribe('order_confirmWithUser()', () => {
it('should confirm an order', async() => {
let stmts = [];
let stmt;
stmts.push('START TRANSACTION');
let params = {
orderFk: 10,
userId: 9
};
// problema: la funcion order_confirmWithUser tiene una transacción, por tanto esta nunca hace rollback
stmt = new ParameterizedSQL('CALL hedera.order_confirmWithUser(?, ?)', [
params.orderFk,
params.userId
]);
stmts.push(stmt);
stmt = new ParameterizedSQL('SELECT confirmed FROM hedera.order WHERE id = ?', [
params.orderFk
]);
let orderIndex = stmts.push(stmt) - 1;
stmts.push('ROLLBACK');
let sql = ParameterizedSQL.join(stmts, ';');
let result = await app.models.Ticket.rawStmt(sql);
savedDescription = result[orderIndex][0].confirmed;
expect(savedDescription).toBeTruthy();
});
});

View File

@ -394,11 +394,18 @@ export default {
intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Intrastat"]',
originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Origin"]',
buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Buyer"]',
densityCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Density"]',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
advancedSearchItemType: 'vn-item-search-panel vn-autocomplete[ng-model="filter.typeFk"]',
advancedSearchButton: 'vn-item-search-panel button[type=submit]',
advancedSmartTableButton: 'vn-item-index vn-button[icon="search"]',
advancedSmartTableGrouping: 'vn-item-index vn-textfield[name=grouping]',
weightByPieceCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Weight/Piece"]',
saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button'
},
itemFixedPrice: {
add: 'vn-fixed-price vn-icon-button[icon="add_circle"]',
firstItemID: 'vn-fixed-price tr:nth-child(2) vn-autocomplete[ng-model="price.itemFk"]',
fourthFixedPrice: 'vn-fixed-price tr:nth-child(5)',
fourthItemID: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.itemFk"]',
fourthWarehouse: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.warehouseFk"]',
@ -408,7 +415,8 @@ export default {
fourthMinPrice: 'vn-fixed-price tr:nth-child(5) > td:nth-child(6) > vn-input-number[ng-model="price.minPrice"]',
fourthStarted: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.started"]',
fourthEnded: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.ended"]',
fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]'
fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]',
orderColumnId: 'vn-fixed-price th[field="itemFk"]'
},
itemCreateView: {
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',
@ -1107,7 +1115,8 @@ export default {
anyBuyLine: 'vn-entry-summary tr.dark-row'
},
entryBasicData: {
reference: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.ref"]',
reference: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.reference"]',
invoiceNumber: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.invoiceNumber"]',
notes: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.notes"]',
observations: 'vn-entry-basic-data vn-textarea[ng-model="$ctrl.entry.observation"]',
supplier: 'vn-entry-basic-data vn-autocomplete[ng-model="$ctrl.entry.supplierFk"]',

View File

@ -0,0 +1,77 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('SmartTable SearchBar integration', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('salesPerson', 'item');
await page.waitToClick(selectors.globalItems.searchButton);
});
afterAll(async() => {
await browser.close();
});
describe('as filters', () => {
it('should search by type in searchBar', async() => {
await page.waitToClick(selectors.itemsIndex.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.itemsIndex.advancedSearchItemType, 'Anthurium');
await page.waitToClick(selectors.itemsIndex.advancedSearchButton);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
});
it('should reload page and have same results', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
});
it('should search by grouping in smartTable', async() => {
await page.waitToClick(selectors.itemsIndex.advancedSmartTableButton);
await page.write(selectors.itemsIndex.advancedSmartTableGrouping, '1');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
});
it('should now reload page and have same results', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
});
});
describe('as orders', () => {
it('should order by first id', async() => {
await page.loginAndModule('developer', 'item');
await page.accessToSection('item.fixedPrice');
await page.doSearch();
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('1');
});
it('should order by last id', async() => {
await page.waitToClick(selectors.itemFixedPrice.orderColumnId);
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('13');
});
it('should reload page and have same order', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('13');
});
});
});

View File

@ -19,6 +19,7 @@ describe('Entry basic data path', () => {
it('should edit the basic data', async() => {
await page.write(selectors.entryBasicData.reference, 'new movement 8');
await page.write(selectors.entryBasicData.invoiceNumber, 'new movement 8');
await page.write(selectors.entryBasicData.notes, 'new notes');
await page.write(selectors.entryBasicData.observations, ' edited');
await page.autocompleteSearch(selectors.entryBasicData.supplier, 'Plants nick');
@ -45,6 +46,13 @@ describe('Entry basic data path', () => {
expect(result).toEqual('new movement 8');
});
it('should confirm the invoiceNumber was edited', async() => {
await page.reloadSection('entry.card.basicData');
const result = await page.waitToGetProperty(selectors.entryBasicData.invoiceNumber, 'value');
expect(result).toEqual('new movement 8');
});
it('should confirm the note was edited', async() => {
const result = await page.waitToGetProperty(selectors.entryBasicData.notes, 'value');

View File

@ -99,6 +99,18 @@ export default class CrudModel extends ModelProxy {
return this.refresh();
}
/**
* Applies a new filter to the model.
*
* @param {Object} params Custom parameters
* @return {Promise} The request promise
*/
applyParams(params) {
this.userParams = params;
return this.refresh();
}
removeFilter() {
return this.applyFilter(null, null);
}

View File

@ -139,8 +139,12 @@ export default class Searchbar extends Component {
}
removeParam(index) {
const field = this.params[index].key;
this.filterSanitizer(field);
this.params.splice(index, 1);
this.doSearch(this.fromBar(), 'bar');
this.toRemove = field;
this.doSearch(this.fromBar(), 'removeBar');
}
fromBar() {
@ -163,7 +167,7 @@ export default class Searchbar extends Component {
let keys = Object.keys(filter);
keys.forEach(key => {
if (key == 'search') return;
if (key == 'search' || key == 'tableQ' || key == 'tableOrder') return;
let value = filter[key];
let chip;
@ -198,6 +202,7 @@ export default class Searchbar extends Component {
let promise = this.onSearch({$params: filter});
promise = promise || this.$q.resolve();
promise.then(data => this.onFilter(filter, source, data));
this.toBar(filter);
}
onFilter(filter, source, data) {
@ -238,8 +243,11 @@ export default class Searchbar extends Component {
} else {
state = this.searchState;
if (filter)
if (filter) {
if (this.tableQ)
filter.tableQ = this.tableQ;
params = {q: JSON.stringify(filter)};
}
if (this.$state.is(state))
opts = {location: 'replace'};
}
@ -247,6 +255,12 @@ export default class Searchbar extends Component {
this.filter = filter;
if (source == 'removeBar') {
delete params[this.toRemove];
delete this.model.userParams[this.toRemove];
this.model.refresh();
}
if (!filter && this.model)
this.model.clear();
if (source != 'state')
@ -269,9 +283,14 @@ export default class Searchbar extends Component {
this.model.clear();
return;
}
if (Object.keys(filter).length === 0) {
this.filterSanitizer('search');
if (this.model.userParams)
delete this.model.userParams['search'];
}
let where = null;
let params = null;
let params = {};
if (this.exprBuilder) {
where = buildFilter(filter,
@ -283,9 +302,89 @@ export default class Searchbar extends Component {
params = this.fetchParams({$params: params});
}
this.tableQ = null;
const hasParams = this.$params.q && Object.keys(JSON.parse(this.$params.q)).length;
if (hasParams) {
const stateFilter = JSON.parse(this.$params.q);
for (let param in stateFilter) {
if (param != 'tableQ' && param != 'orderQ')
this.filterSanitizer(param);
}
for (let param in this.suggestedFilter) {
this.filterSanitizer(param);
delete stateFilter[param];
}
this.tableQ = stateFilter.tableQ;
for (let param in stateFilter.tableQ)
params[param] = stateFilter.tableQ[param];
Object.assign(stateFilter, params);
return this.model.applyParams(params)
.then(() => this.model.data);
}
return this.model.applyFilter(where ? {where} : null, params)
.then(() => this.model.data);
}
filterSanitizer(field) {
if (!field) return;
const userFilter = this.model.userFilter;
const userParams = this.model.userParams;
const where = userFilter && userFilter.where;
if (this.model.userParams)
delete this.model.userParams[field];
if (this.exprBuilder) {
const param = this.exprBuilder({
param: field,
value: null
});
if (param) [field] = Object.keys(param);
}
if (!where) return;
const whereKeys = Object.keys(where);
for (let key of whereKeys) {
removeProp(where, field, key);
if (Object.keys(where).length == 0)
delete userFilter.where;
}
function removeProp(obj, targetProp, prop) {
if (prop == targetProp)
delete obj[prop];
if (prop === 'and' || prop === 'or' && obj[prop]) {
const arrayCopy = obj[prop].slice();
for (let param of arrayCopy) {
const [key] = Object.keys(param);
const index = obj[prop].findIndex(param => {
return Object.keys(param)[0] == key;
});
if (key == targetProp)
obj[prop].splice(index, 1);
if (param[key] instanceof Array)
removeProp(param, field, key);
if (Object.keys(param).length == 0)
obj[prop].splice(index, 1);
}
if (obj[prop].length == 0)
delete obj[prop];
}
}
return {userFilter, userParams};
}
}
ngModule.vnComponent('vnSearchbar', {

View File

@ -6,7 +6,7 @@ describe('Component vnSearchbar', () => {
let $state;
let $params;
let $scope;
let filter = {id: 1, search: 'needle'};
const filter = {id: 1, search: 'needle'};
beforeEach(ngModule('vnCore', $stateProvider => {
$stateProvider
@ -70,8 +70,8 @@ describe('Component vnSearchbar', () => {
describe('filter() setter', () => {
it(`should update the bar params and search`, () => {
let withoutHours = new Date(2000, 1, 1);
let withHours = new Date(withoutHours.getTime());
const withoutHours = new Date(2000, 1, 1);
const withHours = new Date(withoutHours.getTime());
withHours.setHours(12, 30, 15, 10);
controller.filter = {
@ -83,8 +83,8 @@ describe('Component vnSearchbar', () => {
myObjectProp: {myProp: 1}
};
let chips = {};
for (let param of controller.params || [])
const chips = {};
for (const param of controller.params || [])
chips[param.key] = param.chip;
expect(controller.searchString).toBe('needle');
@ -172,13 +172,22 @@ describe('Component vnSearchbar', () => {
describe('removeParam()', () => {
it(`should remove the parameter from the filter`, () => {
jest.spyOn(controller, 'doSearch');
controller.model = {
refresh: jest.fn(),
userParams: {
id: 1
}
};
controller.model.applyParams = jest.fn().mockReturnValue(Promise.resolve());
jest.spyOn(controller.model, 'applyParams');
controller.filter = filter;
controller.removeParam(0);
expect(controller.doSearch).toHaveBeenCalledWith({
search: 'needle'
}, 'bar');
}, 'removeBar');
});
});
@ -199,7 +208,7 @@ describe('Component vnSearchbar', () => {
it(`should go to the summary state when one result`, () => {
jest.spyOn($state, 'go');
let data = [{id: 1}];
const data = [{id: 1}];
controller.baseState = 'foo';
controller.onFilter(filter, 'any', data);
@ -214,7 +223,7 @@ describe('Component vnSearchbar', () => {
$scope.$apply();
jest.spyOn($state, 'go');
let data = [{id: 1}];
const data = [{id: 1}];
controller.baseState = 'foo';
controller.onFilter(filter, 'any', data);
@ -229,7 +238,7 @@ describe('Component vnSearchbar', () => {
$scope.$apply();
jest.spyOn($state, 'go');
let data = [{id: 1}];
const data = [{id: 1}];
controller.baseState = 'foo';
controller.onFilter(filter, 'any', data);
@ -247,7 +256,7 @@ describe('Component vnSearchbar', () => {
controller.onFilter(filter, 'any');
$scope.$apply();
let queryParams = {q: JSON.stringify(filter)};
const queryParams = {q: JSON.stringify(filter)};
expect($state.go).toHaveBeenCalledWith('search.state', queryParams, undefined);
expect(controller.filter).toEqual(filter);

View File

@ -103,3 +103,4 @@
</div>
</tpl-body>
</vn-popover>

View File

@ -15,9 +15,17 @@ export default class SmartTable extends Component {
this.$inputsScope;
this.columns = [];
this.autoSave = false;
this.autoState = true;
this.transclude();
}
$onChanges() {
if (this.model) {
this.defaultFilter();
this.defaultOrder();
}
}
$onDestroy() {
const styleElement = document.querySelector('style[id="smart-table"]');
if (this.$.css && styleElement)
@ -47,10 +55,8 @@ export default class SmartTable extends Component {
set model(value) {
this._model = value;
if (value) {
if (value)
this.$.model = value;
this.defaultOrder();
}
}
getDefaultViewConfig() {
@ -160,8 +166,36 @@ export default class SmartTable extends Component {
}
}
defaultFilter() {
if (this.disabledTableFilter || !this.$params.q) return;
const stateFilter = JSON.parse(this.$params.q).tableQ;
if (!stateFilter || !this.exprBuilder) return;
const columns = this.columns.map(column => column.field);
this.displaySearch();
if (!this.$inputsScope.searchProps)
this.$inputsScope.searchProps = {};
for (let param in stateFilter) {
if (columns.includes(param)) {
const whereParams = {[param]: stateFilter[param]};
Object.assign(this.$inputsScope.searchProps, whereParams);
this.addFilter(param, stateFilter[param]);
}
}
}
defaultOrder() {
const order = this.model.order;
if (this.disabledTableOrder) return;
let stateOrder;
if (this.$params.q)
stateOrder = JSON.parse(this.$params.q).tableOrder;
const order = stateOrder ? stateOrder : this.model.order;
if (!order) return;
const orderFields = order.split(', ');
@ -195,6 +229,9 @@ export default class SmartTable extends Component {
this.setPriority(column.element, priority);
}
}
this.model.order = order;
this.refresh();
}
registerColumns() {
@ -395,30 +432,56 @@ export default class SmartTable extends Component {
}
searchByColumn(field) {
const searchCriteria = this.$inputsScope.searchProps[field];
const emptySearch = searchCriteria === '' || searchCriteria == null;
const filters = this.filterSanitizer(field);
if (filters && filters.userFilter)
this.model.userFilter = filters.userFilter;
if (!emptySearch)
this.addFilter(field, this.$inputsScope.searchProps[field]);
else this.model.refresh();
}
searchPropsSanitizer() {
if (!this.$inputsScope || !this.$inputsScope.searchProps) return null;
let searchProps = this.$inputsScope.searchProps;
const searchPropsArray = Object.entries(searchProps);
searchProps = searchPropsArray.filter(
([key, value]) => value && value != ''
);
return Object.fromEntries(searchProps);
}
addFilter(field, value) {
let where = {[field]: value};
if (value == '') value = null;
let stateFilter = {tableQ: {}};
if (this.$params.q) {
stateFilter = JSON.parse(this.$params.q);
if (!stateFilter.tableQ)
stateFilter.tableQ = {};
delete stateFilter.tableQ[field];
}
const whereParams = {[field]: value};
if (value) {
let where = {[field]: value};
if (this.exprBuilder) {
where = buildFilter(where, (param, value) =>
where = buildFilter(whereParams, (param, value) =>
this.exprBuilder({param, value})
);
}
this.model.addFilter({where});
}
const searchProps = this.searchPropsSanitizer();
Object.assign(stateFilter.tableQ, searchProps);
const params = {q: JSON.stringify(stateFilter)};
this.$state.go(this.$state.current.name, params, {location: 'replace'});
this.refresh();
}
applySort() {
let order = this.sortCriteria.map(criteria => `${criteria.field} ${criteria.sortType}`);
order = order.join(', ');
@ -426,7 +489,18 @@ export default class SmartTable extends Component {
if (order)
this.model.order = order;
this.model.refresh();
let stateFilter = {tableOrder: {}};
if (this.$params.q) {
stateFilter = JSON.parse(this.$params.q);
if (!stateFilter.tableOrder)
stateFilter.tableOrder = {};
}
stateFilter.tableOrder = order;
const params = {q: JSON.stringify(stateFilter)};
this.$state.go(this.$state.current.name, params, {location: 'replace'});
this.refresh();
}
filterSanitizer(field) {
@ -535,6 +609,8 @@ ngModule.vnComponent('smartTable', {
autoSave: '<?',
exprBuilder: '&?',
defaultNewData: '&?',
options: '<?'
options: '<?',
disabledTableFilter: '<?',
disabledTableOrder: '<?',
}
});

View File

@ -9,6 +9,11 @@ describe('Component smartTable', () => {
$httpBackend = _$httpBackend_;
$element = $compile(`<smart-table></smart-table>`)($rootScope);
controller = $element.controller('smartTable');
controller.model = {
refresh: jest.fn().mockReturnValue(new Promise(resolve => resolve())),
addFilter: jest.fn(),
userParams: {}
};
}));
afterEach(() => {
@ -83,7 +88,7 @@ describe('Component smartTable', () => {
describe('defaultOrder', () => {
it('should insert a new object to the controller sortCriteria with a sortType value of "ASC"', () => {
const element = document.createElement('div');
controller.model = {order: 'id'};
controller.model.order = 'id';
controller.columns = [
{field: 'id', element: element},
{field: 'test1', element: element},
@ -101,7 +106,8 @@ describe('Component smartTable', () => {
it('should add new entries to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => {
const element = document.createElement('div');
controller.model = {order: 'test1, id DESC'};
controller.model.order = 'test1, id DESC';
controller.columns = [
{field: 'id', element: element},
{field: 'test1', element: element},
@ -125,8 +131,6 @@ describe('Component smartTable', () => {
describe('addFilter()', () => {
it('should call the model addFilter() with a basic where filter if exprBuilder() was not received', () => {
controller.model = {addFilter: jest.fn()};
controller.addFilter('myField', 'myValue');
const expectedFilter = {
@ -140,7 +144,6 @@ describe('Component smartTable', () => {
it('should call the model addFilter() with a built where filter resultant of exprBuilder()', () => {
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
controller.model = {addFilter: jest.fn()};
controller.addFilter('myField', 'myValue');
@ -155,35 +158,48 @@ describe('Component smartTable', () => {
});
describe('applySort()', () => {
it('should call the model refresh() without making changes on the model order', () => {
controller.model = {refresh: jest.fn()};
it('should call the $state go and model refresh without making changes on the model order', () => {
controller.$state = {
go: jest.fn(),
current: {
name: 'section'
}
};
jest.spyOn(controller, 'refresh');
controller.applySort();
expect(controller.model.order).toBeUndefined();
expect(controller.model.refresh).toHaveBeenCalled();
expect(controller.$state.go).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
});
it('should call the model.refresh() after setting model order according to the controller sortCriteria', () => {
controller.model = {refresh: jest.fn()};
it('should call the $state go and model refresh after setting model order according to the controller sortCriteria', () => {
const orderBy = {field: 'myField', sortType: 'ASC'};
controller.$state = {
go: jest.fn(),
current: {
name: 'section'
}
};
jest.spyOn(controller, 'refresh');
controller.sortCriteria = [orderBy];
controller.applySort();
expect(controller.model.order).toEqual(`${orderBy.field} ${orderBy.sortType}`);
expect(controller.model.refresh).toHaveBeenCalled();
expect(controller.$state.go).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
});
});
describe('filterSanitizer()', () => {
it('should remove the where filter after leaving no fields in it', () => {
controller.model = {
userFilter: {
controller.model.userFilter = {
where: {fieldToRemove: 'valueToRemove'}
},
userParams: {}
};
controller.model.userParams = {};
const result = controller.filterSanitizer('fieldToRemove');
@ -193,8 +209,7 @@ describe('Component smartTable', () => {
});
it('should remove the where filter after leaving no fields and "empty ands/ors" in it', () => {
controller.model = {
userFilter: {
controller.model.userFilter = {
where: {
and: [
{aFieldToRemove: 'aValueToRemove'},
@ -208,8 +223,7 @@ describe('Component smartTable', () => {
]
}
},
userParams: {}
};
controller.model.userParams = {};
const result = controller.filterSanitizer('aFieldToRemove');
@ -219,8 +233,7 @@ describe('Component smartTable', () => {
});
it('should not remove the where filter after leaving no empty "ands/ors" in it', () => {
controller.model = {
userFilter: {
controller.model.userFilter = {
where: {
and: [
{aFieldToRemove: 'aValueToRemove'},
@ -234,9 +247,8 @@ describe('Component smartTable', () => {
],
or: [{dontKillMe: 'thanks'}]
}
},
userParams: {}
};
controller.model.userParams = {};
const result = controller.filterSanitizer('aFieldToRemove');
@ -249,7 +261,7 @@ describe('Component smartTable', () => {
describe('saveAll()', () => {
it('should throw an error if there are no changes to save in the model', () => {
jest.spyOn(controller.vnApp, 'showError');
controller.model = {isChanged: false};
controller.model.isChanged = false;
controller.saveAll();
expect(controller.vnApp.showError).toHaveBeenCalledWith('No changes to save');
@ -258,10 +270,8 @@ describe('Component smartTable', () => {
it('should call the showSuccess() if there are changes to save in the model', done => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.model = {
save: jest.fn().mockReturnValue(Promise.resolve()),
isChanged: true
};
controller.model.save = jest.fn().mockReturnValue(Promise.resolve());
controller.model.isChanged = true;
controller.saveAll().then(() => {
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
@ -269,4 +279,43 @@ describe('Component smartTable', () => {
}).catch(done.fail);
});
});
describe('defaultFilter()', () => {
it('should call model refresh and model addFilter with filter', () => {
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
controller.$params = {
q: '{"tableQ": {"fieldName":"value"}}'
};
controller.columns = [
{field: 'fieldName'}
];
controller.$inputsScope = {
searchProps: {}
};
jest.spyOn(controller, 'refresh');
controller.defaultFilter();
expect(controller.model.addFilter).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
});
});
describe('searchPropsSanitizer()', () => {
it('should searchProps sanitize', () => {
controller.$inputsScope = {
searchProps: {
filterOne: '1',
filterTwo: ''
}
};
const searchPropsExpected = {
filterOne: '1'
};
const newSearchProps = controller.searchPropsSanitizer();
expect(newSearchProps).toEqual(searchPropsExpected);
});
});
});

View File

@ -5,6 +5,8 @@ const crypto = require('crypto');
const nthash = require('smbhash').nthash;
module.exports = Self => {
const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() {
return await Self.findOne({
fields: [
@ -30,6 +32,7 @@ module.exports = Self => {
},
async syncUser(userName, info, password) {
let {
client,
accountConfig
@ -130,12 +133,13 @@ module.exports = Self => {
}));
}
if (changes.length)
if (shouldSync && changes.length)
await client.modify(dn, changes);
} else
} else if (shouldSync)
await client.add(dn, newEntry);
} else {
try {
if (shouldSync)
await client.del(dn);
console.log(` -> User '${userName}' removed from LDAP`);
} catch (e) {
@ -196,10 +200,12 @@ module.exports = Self => {
for (let group of groups) {
try {
let dn = `cn=${group},${groupDn}`;
if (shouldSync) {
await client.modify(dn, new ldap.Change({
operation,
modification: {memberUid: userName}
}));
}
} catch (err) {
if (err.name !== 'NoSuchObjectError')
throw err;
@ -266,8 +272,10 @@ module.exports = Self => {
filter: 'objectClass=posixGroup'
};
let reqs = [];
await client.searchForeach(this.groupDn, opts,
o => reqs.push(client.del(o.dn)));
await client.searchForeach(this.groupDn, opts, object => {
if (shouldSync)
reqs.push(client.del(object.dn));
});
await Promise.all(reqs);
// Recreate roles
@ -291,6 +299,7 @@ module.exports = Self => {
}
let dn = `cn=${role.name},${this.groupDn}`;
if (shouldSync)
reqs.push(client.add(dn, newEntry));
}
await Promise.all(reqs);

View File

@ -71,6 +71,9 @@ module.exports = Self => {
let isEnabled = sambaUser
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE);
if (process.env.NODE_ENV === 'test')
return;
if (info.hasAccount) {
if (!sambaUser) {
await sshClient.exec('samba-tool user create', [

View File

@ -105,7 +105,7 @@
"acl": ["claimManager"]
},
{
"url": "/action",
"url": "/action?q",
"state": "claim.card.action",
"component": "vn-claim-action",
"description": "Action",

View File

@ -406,13 +406,13 @@
}
},
{
"url": "/defaulter",
"url": "/defaulter?q",
"state": "client.defaulter",
"component": "vn-client-defaulter",
"description": "Defaulter"
},
{
"url" : "/notification",
"url" : "/notification?q",
"state": "client.notification",
"component": "vn-client-notification",
"description": "Notifications"
@ -424,7 +424,7 @@
"description": "Unpaid"
},
{
"url": "/extended-list",
"url": "/extended-list?q",
"state": "client.extendedList",
"component": "vn-client-extended-list",
"description": "Extended list"

View File

@ -154,7 +154,8 @@ module.exports = Self => {
e.id,
e.supplierFk,
e.dated,
e.ref,
e.ref reference,
e.ref invoiceNumber,
e.isBooked,
e.isExcludedFromAvailable,
e.notes,

View File

@ -12,10 +12,15 @@ module.exports = Self => {
http: {source: 'path'}
},
{
arg: 'ref',
arg: 'reference',
type: 'string',
description: 'The buyed boxes ids',
},
{
arg: 'invoiceNumber',
type: 'string',
description: 'The registered invoice number',
},
{
arg: 'observation',
type: 'string',
@ -63,7 +68,8 @@ module.exports = Self => {
await entry.updateAttributes({
observation: args.observation,
ref: args.ref
reference: args.reference,
invoiceNumber: args.invoiceNumber
}, myOptions);
const travel = entry.travel();

View File

@ -15,13 +15,15 @@ describe('entry import()', () => {
});
it('should import the buy rows', async() => {
const expectedRef = '1, 2';
const expectedReference = '1, 2';
const expectedInvoiceNumber = '1, 2';
const expectedObservation = '123456';
const ctx = {
req: activeCtx,
args: {
observation: expectedObservation,
ref: expectedRef,
reference: expectedReference,
invoiceNumber: expectedInvoiceNumber,
buys: [
{
itemFk: 1,
@ -58,7 +60,8 @@ describe('entry import()', () => {
}, options);
expect(updatedEntry.observation).toEqual(expectedObservation);
expect(updatedEntry.ref).toEqual(expectedRef);
expect(updatedEntry.reference).toEqual(expectedReference);
expect(updatedEntry.invoiceNumber).toEqual(expectedInvoiceNumber);
expect(entryBuys.length).toEqual(4);
await tx.rollback();

View File

@ -18,8 +18,17 @@
"dated": {
"type": "date"
},
"ref": {
"type": "string"
"reference": {
"type": "string",
"mysql": {
"columnName": "ref"
}
},
"invoiceNumber": {
"type": "string",
"mysql": {
"columnName": "ref"
}
},
"isBooked": {
"type": "boolean"

View File

@ -48,7 +48,7 @@
<vn-textfield
vn-one
label="Reference"
ng-model="$ctrl.entry.ref"
ng-model="$ctrl.entry.reference"
rule
vn-focus>
</vn-textfield>
@ -61,12 +61,20 @@
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
<vn-textfield
vn-one
label="Observation"
ng-model="$ctrl.entry.observation"
rule>
</vn-textarea>
label="Invoice number"
ng-model="$ctrl.entry.invoiceNumber"
rule
vn-focus>
</vn-textfield>
<vn-autocomplete
url="Companies"
label="Company"
show-field="code"
value-field="id"
ng-model="$ctrl.entry.companyFk">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
@ -84,13 +92,14 @@
ng-model="$ctrl.entry.commission"
rule>
</vn-input-number>
<vn-autocomplete
url="Companies"
label="Company"
show-field="code"
value-field="id"
ng-model="$ctrl.entry.companyFk">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
label="Observation"
ng-model="$ctrl.entry.observation"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-check

View File

@ -12,6 +12,7 @@
<vn-th field="id" number>Id</vn-th>
<vn-th field="landed" center expand>Landed</vn-th>
<vn-th>Reference</vn-th>
<vn-th>Invoice number</vn-th>
<vn-th field="supplierFk">Supplier</vn-th>
<vn-th field="isBooked" center>Booked</vn-th>
<vn-th field="isConfirmed" center>Confirmed</vn-th>
@ -45,7 +46,8 @@
{{::entry.landed | date:'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td expand>{{::entry.ref}}</vn-td>
<vn-td expand>{{::entry.reference}}</vn-td>
<vn-td expand>{{::entry.invoiceNumber}}</vn-td>
<vn-td expand>{{::entry.supplierName}}</vn-td>
<vn-td center><vn-check ng-model="entry.isBooked" disabled="true"></vn-check></vn-td>
<vn-td center><vn-check ng-model="entry.isConfirmed" disabled="true"></vn-check></vn-td>

View File

@ -15,3 +15,4 @@ Is inventory: Inventario
Notes: Notas
Status: Estado
Selection: Selección
Invoice number: Núm. factura

View File

@ -13,8 +13,15 @@
<vn-textfield
vn-one
label="Reference"
ng-model="filter.ref">
ng-model="filter.reference">
</vn-textfield>
<vn-textfield
vn-one
label="Invoice number"
ng-model="filter.invoiceNumber">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Travel"

View File

@ -6,3 +6,4 @@ To: Hasta
Agency: Agencia
Warehouse: Almacén
Search entry by id or a suppliers by name or alias: Buscar entrada por id o proveedores por nombre y alias
Invoice number: Núm. factura

View File

@ -27,7 +27,10 @@
value="{{$ctrl.entryData.company.code}}">
</vn-label-value>
<vn-label-value label="Reference"
value="{{$ctrl.entryData.ref}}">
value="{{$ctrl.entryData.reference}}">
</vn-label-value>
<vn-label-value label="Invoice number"
value="{{$ctrl.entryData.invoiceNumber}}">
</vn-label-value>
<vn-label-value label="Notes"
value="{{$ctrl.entryData.notes}}">

View File

@ -8,4 +8,4 @@ Minimum price: Precio mínimo
Buys: Compras
Travel: Envio
Go to the entry: Ir a la entrada
Invoice number: Núm. factura

View File

@ -0,0 +1,53 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('invoiceInEmail', {
description: 'Sends the invoice in email with an attached PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The invoice id',
http: {source: 'path'}
},
{
arg: 'recipient',
type: 'string',
description: 'The recipient email',
required: true,
},
{
arg: 'recipientId',
type: 'number',
description: 'The recipient id to send to the recipient preferred language',
required: false
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: '/:id/invoice-in-email',
verb: 'POST'
}
});
Self.invoiceInEmail = async ctx => {
const args = Object.assign({}, ctx.args);
const params = {
recipient: args.recipient,
lang: ctx.req.getLocale()
};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const email = new Email('invoiceIn', params);
return email.send();
};
};

View File

@ -0,0 +1,50 @@
const {Report} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('invoiceInPdf', {
description: 'Returns the invoiceIn pdf',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/invoice-in-pdf',
verb: 'GET'
}
});
Self.invoiceInPdf = async(ctx, id) => {
const args = Object.assign({}, ctx.args);
const params = {lang: ctx.req.getLocale()};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const report = new Report('invoiceIn', params);
const stream = await report.toPdfStream();
return [stream, 'application/pdf', `filename="doc-${id}.pdf"`];
};
};

View File

@ -4,4 +4,6 @@ module.exports = Self => {
require('../methods/invoice-in/clone')(Self);
require('../methods/invoice-in/toBook')(Self);
require('../methods/invoice-in/getTotals')(Self);
require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self);
};

View File

@ -94,6 +94,11 @@
"model": "Supplier",
"foreignKey": "supplierFk"
},
"supplierContact": {
"type": "hasMany",
"model": "SupplierContact",
"foreignKey": "supplierFk"
},
"currency": {
"type": "belongsTo",
"model": "Currency",

View File

@ -8,6 +8,14 @@ class Controller extends ModuleCard {
{
relation: 'supplier'
},
{
relation: 'supplierContact',
scope: {
where: {
email: {neq: null}
}
}
},
{
relation: 'invoiceInDueDay'
},

View File

@ -10,7 +10,6 @@
translate>
To book
</vn-item>
<vn-item
ng-click="deleteConfirmation.show()"
vn-acl="administrative"
@ -26,6 +25,16 @@
translate>
Clone Invoice
</vn-item>
<vn-item
ng-click="$ctrl.showPdfInvoice()"
translate>
Show agricultural invoice as PDF
</vn-item>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.entity.supplierContact[0].email})"
translate>
Send agricultural invoice as PDF
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
@ -83,3 +92,21 @@
<vn-popup vn-id="summary">
<vn-invoice-in-summary invoice-in="$ctrl.invoiceIn"></vn-invoice-in-summary>
</vn-popup>
<!-- Send PDF invoice confirmation popup -->
<vn-dialog
vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendPdfInvoice($data)"
message="Send PDF invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
label="Email"
ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>

View File

@ -96,6 +96,20 @@ class Controller extends Descriptor {
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn booked')));
}
showPdfInvoice() {
this.vnReport.show(`InvoiceIns/${this.id}/invoice-in-pdf`);
}
sendPdfInvoice($data) {
if (!$data.email)
return this.vnApp.showError(this.$t(`The email can't be empty`));
return this.vnEmail.send(`InvoiceIns/${this.entity.id}/invoice-in-email`, {
recipient: $data.email,
recipientId: this.entity.supplier.id
});
}
}
ngModule.vnComponent('vnInvoiceInDescriptor', {

View File

@ -19,3 +19,5 @@ To book: Contabilizar
Total amount: Total importe
Total net: Total neto
Total stems: Total tallos
Show agricultural invoice as PDF: Ver factura agrícola como PDF
Send agricultural invoice as PDF: Enviar factura agrícola como PDF

View File

@ -34,7 +34,7 @@
<th field="itemFk">
<span translate>Item ID</span>
</th>
<th field="itemName">
<th field="name">
<span translate>Description</span>
</th>
<th field="warehouseFk">

View File

@ -12,14 +12,6 @@ export default class Controller extends Section {
},
defaultSearch: true,
columns: [
{
field: 'itemName',
autocomplete: {
url: 'Items',
showField: 'name',
valueField: 'id'
}
},
{
field: 'warehouseFk',
autocomplete: {
@ -105,8 +97,8 @@ export default class Controller extends Section {
exprBuilder(param, value) {
switch (param) {
case 'itemName':
return {'i.id': value};
case 'name':
return {'i.name': {like: `%${value}%`}};
case 'itemFk':
case 'warehouseFk':
case 'rate2':

View File

@ -23,6 +23,8 @@
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)"
disabled-table-filter="true"
disabled-table-order="true"
class="scrollable sm">
<slot-actions>
<vn-horizontal>

View File

@ -39,7 +39,7 @@
"abstract": true,
"component": "vn-route-card"
}, {
"url": "/agency-term",
"url": "/agency-term?q",
"abstract": true,
"state": "route.agencyTerm",
"component": "ui-view"

View File

@ -51,6 +51,9 @@
"isSerious": {
"type": "boolean"
},
"isTrucker": {
"type": "boolean"
},
"note": {
"type": "string"
},

View File

@ -41,6 +41,7 @@ class Controller extends Descriptor {
'payDay',
'isActive',
'isSerious',
'isTrucker',
'account'
],
include: [

View File

@ -27,6 +27,7 @@ describe('Supplier Component vnSupplierDescriptor', () => {
'payDay',
'isActive',
'isSerious',
'isTrucker',
'account'
],
include: [

View File

@ -118,8 +118,6 @@
rule
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-datalist vn-one
label="Postcode"
ng-model="$ctrl.supplier.postCode"
@ -144,6 +142,8 @@
</vn-icon-button>
</append>
</vn-datalist>
</vn-horizontal>
<vn-horizontal>
<vn-datalist vn-id="town" vn-one
label="City"
ng-model="$ctrl.supplier.city"
@ -159,8 +159,6 @@
({{province.country.country}})
</tpl-item>
</vn-datalist>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-id="province" vn-one
label="Province"
ng-model="$ctrl.supplier.provinceFk"
@ -172,6 +170,8 @@
rule>
<tpl-item>{{name}} ({{country.country}})</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-id="country" vn-one
ng-model="$ctrl.supplier.countryFk"
data="countries"
@ -180,6 +180,10 @@
label="Country"
rule>
</vn-autocomplete>
<vn-check
label="Trucker"
ng-model="$ctrl.supplier.isTrucker">
</vn-check>
</vn-horizontal>
</vn-card>
<vn-button-bar>

View File

@ -3,3 +3,4 @@ Sage transaction type: Tipo de transacción Sage
Sage withholding: Retención Sage
Supplier activity: Actividad proveedor
Healt register: Pasaporte sanitario
Trucker: Transportista

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,6 @@
[
{
"filename": "invoiceIn.pdf",
"component": "invoiceIn"
}
]

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header v-bind="$props"></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{$t('dear')}},</p>
<p v-html="$t('description')"></p>
<p v-html="$t('conclusion')"></p>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer v-bind="$props"></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,11 @@
const Component = require(`vn-print/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
module.exports = {
name: 'invoiceIn',
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build()
}
};

View File

@ -0,0 +1,5 @@
subject: Your agricultural invoice
title: Your agricultural invoice
dear: Dear supplier
description: Attached you can find agricultural receipt generated from your last deliveries. Please return a signed and stamped copy to our administration department.
conclusion: Thanks for your attention!

View File

@ -0,0 +1,5 @@
subject: Tu factura agrícola
title: Tu factura agrícola
dear: Estimado proveedor
description: Adjunto puede encontrar recibo agrícola generado de sus últimas entregas. Por favor, devuelva una copia firmada y sellada a nuestro de departamento de administración.
conclusion: ¡Gracias por tu atención!

View File

@ -0,0 +1,5 @@
subject: Votre facture agricole
title: Votre facture agricole
dear: Cher Fournisseur
description: Vous trouverez en pièce jointe le reçu agricole généré à partir de vos dernières livraisons. Veuillez retourner une copie signée et tamponnée à notre service administratif.
conclusion: Merci pour votre attention!

View File

@ -0,0 +1,5 @@
subject: A sua fatura agrícola
title: A sua fatura agrícola
dear: Caro Fornecedor
description: Em anexo encontra-se o recibo agrícola gerado a partir das suas últimas entregas. Por favor, devolva uma cópia assinada e carimbada ao nosso departamento de administração.
conclusion: Obrigado pela atenção.

View File

@ -38,3 +38,8 @@ h2 {
.phytosanitary-info {
margin-top: 10px
}
.observations{
text-align: justify;
text-justify: inter-word;
}

View File

@ -1,13 +1,13 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Header block -->
<report-header v-bind="$props"
v-bind:company-code="ticket.companyCode">
<report-header v-bind="$props" v-bind:company-code="ticket.companyCode">
</report-header>
<!-- Block -->
<div class="grid-row">
@ -88,10 +88,13 @@
<td width="5%">{{sale.itemFk | zerofill('000000')}}</td>
<td class="number">{{sale.quantity}}</td>
<td width="50%">{{sale.concept}}</td>
<td class="number" v-if="showPrices">{{sale.price | currency('EUR', $i18n.locale)}}</td>
<td class="centered" width="5%" v-if="showPrices">{{(sale.discount / 100) | percentage}}</td>
<td class="number" v-if="showPrices">{{sale.price | currency('EUR',
$i18n.locale)}}</td>
<td class="centered" width="5%" v-if="showPrices">{{(sale.discount / 100) |
percentage}}</td>
<td class="centered" v-if="showPrices">{{sale.vatType}}</td>
<td class="number" v-if="showPrices">{{sale.price * sale.quantity * (1 - sale.discount / 100) | currency('EUR', $i18n.locale)}}</td>
<td class="number" v-if="showPrices">{{sale.price * sale.quantity * (1 -
sale.discount / 100) | currency('EUR', $i18n.locale)}}</td>
</tr>
<tr class="description font light-gray">
<td colspan="7">
@ -139,10 +142,12 @@
<td width="5%"></td>
<td class="number">{{service.quantity}}</td>
<td width="50%">{{service.description}}</td>
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}</td>
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}
</td>
<td class="centered" width="5%"></td>
<td class="centered">{{service.taxDescription}}</td>
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}</td>
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}
</td>
</tr>
</tbody>
<tfoot>
@ -219,7 +224,8 @@
</tr>
<tr class="font bold">
<td colspan="2">{{$t('total')}}</td>
<td colspan="2" class="number">{{getTotal() | currency('EUR', $i18n.locale)}}</td>
<td colspan="2" class="number">{{getTotal() | currency('EUR',
$i18n.locale)}}</td>
</tr>
</tfoot>
</table>
@ -233,10 +239,10 @@
<div class="flag">
<div class="columns">
<div class="size25">
<img v-bind:src="getReportSrc('europe.png')"/>
<img v-bind:src="getReportSrc('europe.png')" />
</div>
<div class="size75 flag-text">
<strong>{{$t('plantPassport')}}</strong><br/>
<strong>{{$t('plantPassport')}}</strong><br />
</div>
</div>
</div>
@ -269,25 +275,30 @@
<div id="signature" class="panel" v-if="signature && signature.id">
<div class="header">{{$t('digitalSignature')}}</div>
<div class="body centered">
<img v-bind:src="dmsPath"/>
<img v-bind:src="dmsPath" />
<div>{{signature.created | date('%d-%m-%Y')}}</div>
</div>
</div>
</div>
<!-- End of signature block -->
</div>
<div class="columns vn-mb-ml" v-if="hasObservations">
<!-- Observations block-->
<div class="size100 no-page-break">
<h2>{{$t('observations')}}</h2>
<p class="observations">{{ticket.description}}</p>
</div>
</div>
</div>
</div>
<!-- Footer block -->
<report-footer id="pageFooter"
v-bind:company-code="ticket.companyCode"
v-bind:left-text="footerType"
v-bind:center-text="client.socialName"
v-bind="$props">
<report-footer id="pageFooter" v-bind:company-code="ticket.companyCode"
v-bind:left-text="footerType" v-bind:center-text="client.socialName" v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</body>
</html>

View File

@ -54,7 +54,11 @@ module.exports = {
footerType() {
const translatedType = this.$t(this.deliverNoteType);
return `${translatedType} ${this.id}`;
},
hasObservations() {
return this.ticket.description !== null;
}
},
methods: {
fetchClient(id) {

View File

@ -47,3 +47,4 @@ taxes:
tfoot:
subtotal: Subtotal
total: Total
observations: Observations

View File

@ -48,3 +48,4 @@ taxes:
tfoot:
subtotal: Subtotal
total: Total
observations: Observaciones

View File

@ -48,3 +48,4 @@ taxes:
tfoot:
subtotal: Total partiel
total: Total
observations: Observations

View File

@ -48,3 +48,4 @@ taxes:
tfoot:
subtotal: Subtotal
total: Total
observations: Observações

View File

@ -2,7 +2,11 @@ SELECT
t.id,
t.shipped,
c.code companyCode,
t.packages
t.packages,
tto.description
FROM ticket t
JOIN company c ON c.id = t.companyFk
LEFT JOIN ticketObservation tto
ON tto.ticketFk = t.id
AND tto.observationTypeFk = (SELECT id FROM observationType WHERE code = 'deliveryNote')
WHERE t.id = ?

View File

@ -264,7 +264,7 @@
<tbody>
<tr v-for="row in intrastat">
<td>{{row.code}}</td>
<td width="50%">{{row.description}}</td>
<td width="50%">{{row.description || $t('services') }}</td>
<td class="number">{{row.stems | number($i18n.locale)}}</td>
<td class="number">{{row.netKg | number($i18n.locale)}}</td>
<td class="number">{{row.subtotal | currency('EUR', $i18n.locale)}}</td>

View File

@ -81,7 +81,7 @@ module.exports = {
return this.rawSqlFromDef(`taxes`, [reference]);
},
fetchIntrastat(reference) {
return this.rawSqlFromDef(`intrastat`, [reference, reference, reference]);
return this.rawSqlFromDef(`intrastat`, [reference, reference, reference, reference]);
},
fetchRectified(reference) {
return this.rawSqlFromDef(`rectified`, [reference]);

View File

@ -34,3 +34,4 @@ plantPassport: Plant passport
observations: Observations
wireTransfer: "Pay method: Transferencia"
accountNumber: "Account number: {0}"
services: Services

View File

@ -34,3 +34,4 @@ plantPassport: Pasaporte fitosanitario
observations: Observaciones
wireTransfer: "Forma de pago: Transferencia"
accountNumber: "Número de cuenta: {0}"
services: Servicios

View File

@ -1,4 +1,4 @@
SELECT
(SELECT
ir.id code,
ir.description description,
CAST(SUM(IFNULL(i.stems, 1) * s.quantity) AS DECIMAL(10,2)) stems,
@ -19,4 +19,14 @@ SELECT
WHERE t.refFk = ?
AND i.intrastatFk
GROUP BY i.intrastatFk
ORDER BY i.intrastatFk;
ORDER BY i.intrastatFk)
UNION ALL
(SELECT
NULL AS code,
NULL AS description,
0 AS stems,
0 AS netKg,
CAST(SUM((ts.quantity * ts.price)) AS DECIMAL(10,2)) AS subtotal
FROM vn.ticketService ts
JOIN vn.ticket t ON ts.ticketFk = t.id
WHERE t.refFk = ?);

View File

@ -0,0 +1,12 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/report.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,42 @@
h2 {
font-weight: 100;
color: #555
}
.table-title {
margin-bottom: 15px;
font-size: .8rem
}
.table-title h2 {
margin: 0 15px 0 0
}
.ticket-info {
font-size: 22px
}
#nickname h2 {
max-width: 400px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
#phytosanitary {
padding-right: 10px
}
#phytosanitary .flag img {
width: 100%
}
#phytosanitary .flag .flag-text {
padding-left: 10px;
box-sizing: border-box;
}
.phytosanitary-info {
margin-top: 10px
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Header block -->
<report-header v-bind="$props"
v-bind:company-code="invoice.companyCode">
</report-header>
<!-- Block -->
<div class="grid-row">
<div class="grid-block">
<div class="columns vn-mb-lg">
<div class="size50">
<div class="size75 vn-mt-ml">
<h1 class="title uppercase">{{$t('title')}}</h1>
<table class="row-oriented ticket-info">
<tbody>
<tr>
<td class="font gray uppercase">{{$t('supplierId')}}</td>
<th>{{invoice.supplierId}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('invoiceId')}}</td>
<th>{{invoice.id}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('date')}}</td>
<th>{{invoice.created | date('%d-%m-%Y')}}</th>
</tr>
</tbody>
</table>
</div>
</div>
<div class="size50">
<div class="panel">
<div class="header">{{$t('invoiceData')}}</div>
<div class="body">
<h3 class="uppercase">{{invoice.name}}</h3>
<div>
{{invoice.postalAddress}}
</div>
<div>
{{invoice.postcodeCity}}
</div>
<div v-if="invoice.nif">
{{$t('fiscalId')}}: {{invoice.nif}}
</div>
<div v-if="invoice.phone">
{{$t('phone')}}: {{invoice.phone}}
</div>
</div>
</div>
</div>
</div>
<div class="vn-mt-lg" v-for="entry in entries">
<div class="table-title clearfix">
<div class="pull-left">
<h2>{{$t('invoiceId')}}</strong>
</div>
<div class="pull-left vn-mr-md">
<div class="field rectangle">
<span>{{entry.id}}</span>
</div>
</div>
<div class="pull-left">
<h2>{{$t('date')}}</h2>
</div>
<div class="pull-left">
<div class="field rectangle">
<span>{{entry.landed | date}}</span>
</div>
</div>
<span id="nickname" class="pull-right">
<div class="pull-left">
<h2>{{$t('reference')}}</h2>
</div>
<div class="pull-left">
<div class="field rectangle">
<span>{{entry.ref}}</span>
</div>
</div>
</span>
</div>
<table class="column-oriented">
<thead>
<tr>
<th width="50%">{{$t('item')}}</th>
<th class="number">{{$t('quantity')}}</th>
<th class="number">{{$t('buyingValue')}}</th>
<th class="number">{{$t('amount')}}</th>
</tr>
</thead>
<tbody v-for="buy in entry.buys" class="no-page-break">
<tr>
<td width="50%">{{buy.name}}</td>
<td class="number">{{buy.quantity}}</td>
<td class="number">{{buy.buyingValue}}</td>
<td class="number">{{buyImport(buy) | currency('EUR', $i18n.locale)}}</td>
</tr>
<tr class="description font light-gray">
<td colspan="4">
<span v-if="buy.value5">
<strong>{{buy.tag5}}</strong> {{buy.value5}}
</span>
<span v-if="buy.value6">
<strong>{{buy.tag6}}</strong> {{buy.value6}}
</span>
<span v-if="buy.value7">
<strong>{{buy.tag7}}</strong> {{buy.value7}}
</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="font bold">
<span class="pull-right">{{$t('subtotal')}}</span>
</td>
<td class="number">{{entrySubtotal(entry) | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
</div>
<!-- End of sales block -->
<div class="columns vn-mt-xl">
<!-- Taxes block -->
<div id="taxes" class="size50 pull-right no-page-break" v-if="taxes">
<table class="column-oriented">
<thead>
<tr>
<th colspan="4">{{$t('taxBreakdown')}}</th>
</tr>
</thead>
<thead class="light">
<tr>
<th width="45%">{{$t('type')}}</th>
<th width="25%" class="number">
{{$t('taxBase')}}
</th>
<th>{{$t('tax')}}</th>
<th class="number">{{$t('fee')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="tax in taxes">
<td width="45%">{{tax.name}}</td>
<td width="25%" class="number">
{{tax.taxableBase | currency('EUR', $i18n.locale)}}
</td>
<td>{{tax.rate | percentage}}</td>
<td class="number">{{tax.vat | currency('EUR', $i18n.locale)}}</td>
</tr>
</tbody>
<tfoot>
<tr class="font bold">
<td width="45%">{{$t('subtotal')}}</td>
<td width="20%" class="number">
{{sumTotal(taxes, 'taxableBase') | currency('EUR', $i18n.locale)}}
</td>
<td></td>
<td class="number">{{sumTotal(taxes, 'vat') | currency('EUR', $i18n.locale)}}</td>
</tr>
<tr class="font bold">
<td colspan="2">{{$t('total')}}</td>
<td colspan="2" class="number">{{taxTotal() | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
<div class="panel" v-if="invoice.footNotes">
<div class="header">{{$t('notes')}}</div>
<div class="body">
<span>{{invoice.footNotes}}</span>
</div>
</div>
</div>
<!-- End of taxes block -->
<!-- Observations block -->
<div class="columns vn-mt-xl">
<div class="size50 pull-left no-page-break" >
<div class="panel" >
<div class="header">{{$t('observations')}}</div>
<div class="body">
<div>{{$t('payMethod')}}</div>
<div>{{invoice.payMethod}}</div>
</div>
</div>
</div>
</div>
<!-- End of observations block -->
</div>
</div>
</div>
<!-- Footer block -->
<report-footer id="pageFooter"
v-bind:company-code="invoice.companyCode"
v-bind:left-text="$t('invoiceId')"
v-bind:center-text="invoice.name"
v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,94 @@
const Component = require(`vn-print/core/component`);
const reportHeader = new Component('report-header');
const reportFooter = new Component('report-footer');
module.exports = {
name: 'invoiceIn',
async serverPrefetch() {
this.invoice = await this.fetchInvoice(this.id);
this.taxes = await this.fetchTaxes(this.id);
if (!this.invoice)
throw new Error('Something went wrong');
const entries = await this.fetchEntry(this.id);
const buys = await this.fetchBuy(this.id);
const map = new Map();
for (let entry of entries) {
entry.buys = [];
map.set(entry.id, entry);
}
for (let buy of buys) {
const entry = map.get(buy.entryFk);
if (entry) entry.buys.push(buy);
}
this.entries = entries;
},
computed: {
},
methods: {
fetchInvoice(id) {
return this.findOneFromDef('invoice', [id]);
},
fetchEntry(id) {
return this.rawSqlFromDef('entry', [id]);
},
fetchBuy(id) {
return this.rawSqlFromDef('buy', [id]);
},
async fetchTaxes(id) {
const taxes = await this.rawSqlFromDef(`taxes`, [id]);
return this.taxVat(taxes);
},
buyImport(buy) {
return buy.quantity * buy.buyingValue;
},
entrySubtotal(entry) {
let subTotal = 0.00;
for (let buy of entry.buys)
subTotal += this.buyImport(buy);
return subTotal;
},
sumTotal(rows, prop) {
let total = 0.00;
for (let row of rows)
total += parseFloat(row[prop]);
return total;
},
taxVat(taxes) {
for (tax of taxes) {
let vat = 0;
if (tax.rate && tax.taxableBase)
vat = (tax.rate / 100) * tax.taxableBase;
tax.vat = vat;
}
return taxes;
},
taxTotal() {
const base = this.sumTotal(this.taxes, 'taxableBase');
const vat = this.sumTotal(this.taxes, 'vat');
return base + vat;
}
},
components: {
'report-header': reportHeader.build(),
'report-footer': reportFooter.build(),
},
props: {
id: {
type: Number,
description: 'The invoice id'
}
}
};

View File

@ -0,0 +1,25 @@
reportName: invoice
title: Agricultural invoice
invoiceId: Agricultural invoice
supplierId: Proveedor
invoiceData: Invoice data
reference: Reference
fiscalId: FI / NIF
phone: Phone
date: Date
item: Item
quantity: Qty.
concept: Concept
buyingValue: PSP/u
discount: Disc.
vat: VAT
amount: Amount
type: Type
taxBase: Tax base
tax: Tax
fee: Fee
total: Total
subtotal: Subtotal
taxBreakdown: Tax breakdown
observations: Observations
payMethod: Pay method

View File

@ -0,0 +1,25 @@
reportName: factura
title: Factura Agrícola
invoiceId: Factura Agrícola
supplierId: Proveedor
invoiceData: Datos de facturación
reference: Referencia
fiscalId: CIF / NIF
phone: Tlf
date: Fecha
item: Artículo
quantity: Cant.
concept: Concepto
buyingValue: PVP/u
discount: Dto.
vat: IVA
amount: Importe
type: Tipo
taxBase: Base imp.
tax: Tasa
fee: Cuota
total: Total
subtotal: Subtotal
taxBreakdown: Desglose impositivo
observations: Observaciones
payMethod: Método de pago

View File

@ -0,0 +1,18 @@
SELECT
b.id,
e.id entryFk,
it.name,
b.quantity,
b.buyingValue,
(b.quantity * b.buyingValue) total,
it.tag5,
it.value5,
it.tag6,
it.value6,
it.tag7,
it.value7
FROM entry e
JOIN invoiceIn i ON i.id = e.invoiceInFk
JOIN buy b ON b.entryFk = e.id
JOIN item it ON it.id = b.itemFk
WHERE i.id = ?

View File

@ -0,0 +1,8 @@
SELECT
e.id,
t.landed,
e.ref
FROM entry e
JOIN invoiceIn i ON i.id = e.invoiceInFk
JOIN travel t ON t.id = e.travelFk
WHERE i.id = ?

View File

@ -0,0 +1,14 @@
SELECT
i.id,
s.id supplierId,
i.created,
s.name,
s.street AS postalAddress,
s.nif,
s.phone,
p.name payMethod
FROM invoiceIn i
JOIN supplier s ON s.id = i.supplierFk
JOIN company c ON c.id = i.companyFk
JOIN payMethod p ON p.id = s.payMethodFk
WHERE i.id = ?

View File

@ -0,0 +1,8 @@
SELECT
ti.iva as name,
ti.PorcentajeIva as rate,
iit.taxableBase
FROM invoiceIn ii
JOIN invoiceInTax iit ON ii.id = iit.invoiceInFk
JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
WHERE ii.id = ?;