Merge branch 'dev' into 2883-invoiceIn-Booking
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2022-01-24 15:40:37 +01:00
commit 5fbbae7ac6
108 changed files with 1267 additions and 768 deletions

View File

@ -8,7 +8,7 @@ ALTER TABLE `vn`.`ticket` AUTO_INCREMENT = 1;
INSERT INTO `salix`.`AccessToken` (`id`, `ttl`, `created`, `userId`)
VALUES
('TOTALLY_SECURE_TOKEN', '1209600', CURDATE(), 66);
('DEFAULT_TOKEN', '1209600', CURDATE(), 66);
INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`)
@ -104,17 +104,17 @@ INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`)
(3, 'GBP', 'Libra', 1),
(4, 'JPY', 'Yen Japones', 1);
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`)
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`)
VALUES
(1, 'España', 1, 'ES', 1, 24, 4),
(2, 'Italia', 1, 'IT', 1, 27, 4),
(3, 'Alemania', 1, 'DE', 1, 22, 4),
(4, 'Rumania', 1, 'RO', 1, 24, 4),
(5, 'Holanda', 1, 'NL', 1, 18, 4),
(8, 'Portugal', 1, 'PT', 1, 27, 4),
(13,'Ecuador', 0, 'EC', 1, 24, 2),
(19,'Francia', 1, 'FR', 1, 27, 4),
(30,'Canarias', 1, 'IC', 1, 24, 4);
(1, 'España', 1, 'ES', 1, 24, 4, 0, 1),
(2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1),
(3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1),
(4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1),
(5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1),
(8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1),
(13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2),
(19,'Francia', 1, 'FR', 1, 27, 4, 0, 1),
(30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2);
INSERT INTO `hedera`.`language` (`code`, `name`, `orgName`, `isActive`)
VALUES
@ -243,7 +243,7 @@ INSERT INTO `vn`.`province`(`id`, `name`, `countryFk`, `autonomyFk`, `warehouseF
VALUES
(1, 'Province one', 1, 1, NULL),
(2, 'Province two', 1, 1, NULL),
(3, 'Province three', 1, 2, NULL),
(3, 'Province three', 30, 2, NULL),
(4, 'Province four', 2, 3, NULL),
(5, 'Province five', 13, 4, NULL);
@ -455,7 +455,8 @@ INSERT INTO `vn`.`creditInsurance`(`id`, `creditClassification`, `credit`, `crea
INSERT INTO `vn`.`companyGroup`(`id`, `code`)
VALUES
(1, 'Wayne Industries');
(1, 'wayneIndustries'),
(2, 'Verdnatura');
INSERT INTO `vn`.`bankEntity`(`id`, `countryFk`, `name`, `bic`)
VALUES
@ -466,13 +467,13 @@ INSERT INTO `vn`.`supplierAccount`(`id`, `supplierFk`, `iban`, `bankEntityFk`)
VALUES
(241, 442, 'ES111122333344111122221111', 128);
INSERT INTO `vn`.`company`(`id`, `code`, `supplierAccountFk`, `workerManagerFk`, `companyCode`, `sage200Company`, `expired`, `phytosanitary`)
INSERT INTO `vn`.`company`(`id`, `code`, `supplierAccountFk`, `workerManagerFk`, `companyCode`, `sage200Company`, `expired`, `companyGroupFk`, `phytosanitary`)
VALUES
(69 , 'CCs', NULL, 30, NULL, 0, NULL, NULL),
(442 , 'VNL', 241, 30, 2 , 1, NULL, 'VNL Company - Plant passport'),
(567 , 'VNH', NULL, 30, NULL, 4, NULL, 'VNH Company - Plant passport'),
(791 , 'FTH', NULL, 30, NULL, 3, '2015-11-30', NULL),
(1381, 'ORN', NULL, 30, NULL, 7, NULL, 'ORN Company - Plant passport');
(69 , 'CCs', NULL, 30, NULL, 0, NULL, 1, NULL),
(442 , 'VNL', 241, 30, 2 , 1, NULL, 2, 'VNL Company - Plant passport'),
(567 , 'VNH', NULL, 30, NULL, 4, NULL, 1, 'VNH Company - Plant passport'),
(791 , 'FTH', NULL, 30, NULL, 3, '2015-11-30', 1, NULL),
(1381, 'ORN', NULL, 30, NULL, 7, NULL, 1, 'ORN Company - Plant passport');
INSERT INTO `vn`.`taxArea` (`code`, `claveOperacionFactura`, `CodigoTransaccion`)
VALUES
@ -486,7 +487,9 @@ INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaF
('A', 'Global nacional', 1, 'NATIONAL', 0),
('T', 'Española rapida', 1, 'NATIONAL', 0),
('V', 'Intracomunitaria global', 0, 'CEE', 1),
('M', 'Múltiple nacional', 1, 'NATIONAL', 0);
('M', 'Múltiple nacional', 1, 'NATIONAL', 0),
('E', 'Exportación rápida', 0, 'WORLD', 0);
;
INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`)
VALUES

View File

@ -43,4 +43,10 @@
&.disabled.checked > .btn {
background-color: $color-font-secondary;
}
&[triple-state]:not(.indeterminate):not(.checked) {
.btn {
background-color: lighten($color-alert, 5%);
}
}
}

View File

@ -171,9 +171,10 @@ export default class SmartTable extends Component {
if (field.length === 2)
sortType = field[1];
const priority = this.sortCriteria.length + 1;
const column = this.columns.find(column => column.field == fieldName);
if (column) {
this.sortCriteria.push({field: fieldName, sortType: sortType});
this.sortCriteria.push({field: fieldName, sortType: sortType, priority: priority});
const isASC = sortType == 'ASC';
const isDESC = sortType == 'DESC';
@ -187,6 +188,8 @@ export default class SmartTable extends Component {
column.element.classList.remove('desc');
column.element.classList.add('asc');
}
this.setPriority(column.element, priority);
}
}
}
@ -241,9 +244,13 @@ export default class SmartTable extends Component {
const isDESC = existingCriteria && existingCriteria.sortType == 'DESC';
if (!existingCriteria) {
this.sortCriteria.push({field: field, sortType: 'ASC'});
const priority = this.sortCriteria.length + 1;
this.sortCriteria.push({field: field, sortType: 'ASC', priority: priority});
element.classList.remove('desc');
element.classList.add('asc');
this.setPriority(element, priority);
}
if (isDESC) {
@ -252,6 +259,8 @@ export default class SmartTable extends Component {
}), 1);
element.classList.remove('desc');
element.classList.remove('asc');
element.querySelector('sort-priority').remove();
}
if (isASC) {
@ -260,9 +269,29 @@ export default class SmartTable extends Component {
element.classList.add('desc');
}
let priority = 0;
for (const criteria of this.sortCriteria) {
const column = this.columns.find(column => column.field == criteria.field);
if (column) {
criteria.priority = priority;
priority++;
column.element.querySelector('sort-priority').remove();
this.setPriority(column.element, priority);
}
}
this.applySort();
}
setPriority(column, priority) {
const sortPriority = document.createElement('sort-priority');
sortPriority.setAttribute('class', 'sort-priority');
sortPriority.innerHTML = priority;
column.appendChild(sortPriority);
}
displaySearch() {
const header = this.element.querySelector('thead > tr');
if (!header) return;

View File

@ -96,9 +96,10 @@ describe('Component smartTable', () => {
expect(firstSortCriteria.field).toEqual('id');
expect(firstSortCriteria.sortType).toEqual('ASC');
expect(firstSortCriteria.priority).toEqual(1);
});
it('should insert two new objects to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => {
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.columns = [
@ -114,8 +115,11 @@ describe('Component smartTable', () => {
expect(firstSortCriteria.field).toEqual('test1');
expect(firstSortCriteria.sortType).toEqual('ASC');
expect(firstSortCriteria.priority).toEqual(1);
expect(secondSortCriteria.field).toEqual('id');
expect(secondSortCriteria.sortType).toEqual('DESC');
expect(secondSortCriteria.priority).toEqual(2);
});
});

View File

@ -9,7 +9,7 @@ smart-table {
}
th[field][number] {
& > :before {
& > span:before {
vertical-align: middle;
font-family: 'Material Icons';
content: 'arrow_downward';
@ -19,26 +19,26 @@ smart-table {
}
&.asc > :before, &.desc > :before {
&.asc > span:before, &.desc > span:before {
color: $color-font;
opacity: 1;
}
&.asc > :before {
&.asc > span:before {
content: 'arrow_upward';
}
&.desc > :before {
&.desc > span:before {
content: 'arrow_downward';
}
&:hover > :before {
&:hover > span:before {
opacity: 1;
}
}
th[field]:not([number]) {
& > :after {
& > span:after {
vertical-align: middle;
font-family: 'Material Icons';
content: 'arrow_downward';
@ -48,20 +48,20 @@ smart-table {
}
&.asc > :after, &.desc > :after {
&.asc > span:after, &.desc > span:after {
color: $color-font;
opacity: 1;
}
&.asc > :after {
&.asc > span:after {
content: 'arrow_upward';
}
&.desc > :after {
&.desc > span:after {
content: 'arrow_downward';
}
&:hover > :after {
&:hover > span:after {
opacity: 1;
}
}
@ -144,3 +144,15 @@ smart-table {
width: 33%
}
}
.sort-priority {
background-color: $color-font-bg-marginal;
border-radius: 50%;
padding: 2px 5px;
display: inline-block;
text-align: center;
width: 7px;
height: 13px;
font-size: 10px;
color: $color-font-bg
}

View File

@ -35,6 +35,13 @@
{{::agencyModeName}} - {{::warehouseInName}} ({{::shipped | date: 'dd/MM/yyyy'}}) →
{{::warehouseOutName}} ({{::landed | date: 'dd/MM/yyyy'}})
</tpl-item>
<append>
<vn-icon-button
icon="filter_alt"
vn-click-stop="$ctrl.showFilterDialog($ctrl.entry.travelFk)"
vn-tooltip="Filter...">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
@ -122,3 +129,93 @@
</vn-button>
</vn-button-bar>
</form>
<!-- Filter travel dialog -->
<vn-dialog
vn-id="filterDialog"
message="Filter travel">
<tpl-body class="travelFilter">
<vn-horizontal>
<vn-autocomplete
label="Agency"
ng-model="$ctrl.travelFilterParams.agencyFk"
url="AgencyModes"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete
label="Warehouse Out"
ng-model="$ctrl.travelFilterParams.warehouseOutFk"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete
label="Warehouse In"
ng-model="$ctrl.travelFilterParams.warehouseInFk"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-date-picker
label="Shipped"
ng-model="$ctrl.travelFilterParams.shipped">
</vn-date-picker>
<vn-date-picker
label="Landed"
ng-model="$ctrl.travelFilterParams.landed">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="vn-mb-md">
<vn-button vn-none
label="Search"
ng-click="$ctrl.filter()">
</vn-button>
</vn-horizontal>
<vn-crud-model
vn-id="travelsModel"
url="Travels"
filter="$ctrl.travelFilter"
data="travels"
limit="10">
</vn-crud-model>
<vn-data-viewer
model="travelsModel"
class="vn-w-lg">
<vn-table class="scrollable">
<vn-thead>
<vn-tr>
<vn-th shrink>ID</vn-th>
<vn-th expand>Agency</vn-th>
<vn-th expand>Warehouse Out</vn-th>
<vn-th expand>Warehouse In</vn-th>
<vn-th expand>Shipped</vn-th>
<vn-th expand>Landed</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="travel in travels"
class="clickable vn-tr search-result"
ng-click="$ctrl.selectTravel(travel.id)">
<vn-td shrink>
<span
vn-click-stop="travelDescriptor.show($event, travel.id)"
class="link">
{{::travel.id}}
</span>
</vn-td>
<vn-td expand>{{::travel.agency.name}}</vn-td>
<vn-td expand>{{::travel.warehouseOut.name}}</vn-td>
<vn-td expand>{{::travel.warehouseIn.name}}</vn-td>
<vn-td expand>{{::travel.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td expand>{{::travel.landed | date: 'dd/MM/yyyy'}}</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-data-viewer>
<vn-travel-descriptor-popover
vn-id="travel-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-travel-descriptor-popover>
</tpl-body>
</vn-dialog>

View File

@ -1,10 +1,68 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
showFilterDialog(travel) {
this.activeTravel = travel;
this.travelFilterParams = {};
this.travelFilter = {
include: [
{
relation: 'agency',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseIn',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseOut',
scope: {
fields: ['name']
}
}
]
};
this.$.filterDialog.show();
}
selectTravel(id) {
this.entry.travelFk = id;
this.$.filterDialog.hide();
}
filter() {
const filter = this.travelFilter;
const params = this.travelFilterParams;
const where = {};
for (let key in params) {
const value = params[key];
if (!value) continue;
switch (key) {
case 'agencyFk':
case 'warehouseInFk':
case 'warehouseOutFk':
case 'shipped':
case 'landed':
where[key] = value;
}
}
filter.where = where;
this.$.travelsModel.applyFilter(filter);
}
}
ngModule.vnComponent('vnEntryBasicData', {
template: require('./index.html'),
controller: Section,
bindings: {
entry: '<'
}
},
controller: Controller
});

View File

@ -0,0 +1,3 @@
.travelFilter{
width: 950px;
}

View File

@ -21,6 +21,20 @@
ng-model="filter.fi">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one ng-model="filter.supplierFk"
url="Suppliers"
show-field="nickname"
search-function="{or: [{id: $search}, {nickname: {like: '%'+ $search +'%'}}]}"
value-field="id"
order="nickname"
label="Supplier">
<tpl-item>
{{::id}} - {{::nickname}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one

View File

@ -1,7 +1,7 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const got = require('got');
const path = require('path');
const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('createPdf', {
@ -57,39 +57,37 @@ module.exports = Self => {
hasPdf: true
}, myOptions);
const response = got.stream(`${origin}/api/report/invoice`, {
searchParams: {
return axios.get(`${origin}/api/report/invoice`, {
responseType: 'stream',
params: {
authorization: auth.id,
invoiceId: id
}
});
}).then(async response => {
const issued = invoiceOut.issued;
const year = issued.getFullYear().toString();
const month = (issued.getMonth() + 1).toString();
const day = issued.getDate().toString();
const issued = invoiceOut.issued;
const year = issued.getFullYear().toString();
const month = (issued.getMonth() + 1).toString();
const day = issued.getDate().toString();
const container = await models.InvoiceContainer.container(year);
const rootPath = container.client.root;
const fileName = `${year}${invoiceOut.ref}.pdf`;
const src = path.join(rootPath, year, month, day);
fileSrc = path.join(src, fileName);
const container = await models.InvoiceContainer.container(year);
const rootPath = container.client.root;
const fileName = `${year}${invoiceOut.ref}.pdf`;
const src = path.join(rootPath, year, month, day);
fileSrc = path.join(src, fileName);
await fs.mkdir(src, {recursive: true});
await fs.mkdir(src, {recursive: true});
if (tx) await tx.commit();
if (tx) await tx.commit();
response.data.pipe(fs.createWriteStream(fileSrc));
}).catch(async e => {
if (fs.existsSync(fileSrc))
await fs.unlink(fileSrc);
const writeStream = fs.createWriteStream(fileSrc);
writeStream.on('open', () => response.pipe(writeStream));
writeStream.on('finish', () => writeStream.end());
return new Promise(resolve => {
writeStream.on('close', () => resolve(invoiceOut));
throw e;
});
} catch (e) {
if (tx) await tx.rollback();
if (fs.existsSync(fileSrc))
await fs.unlink(fileSrc);
throw e;
}
};

View File

@ -1,24 +1,28 @@
const models = require('vn-loopback/server/server').models;
const got = require('got');
const LoopBackContext = require('loopback-context');
const fs = require('fs-extra');
const axios = require('axios');
describe('InvoiceOut createPdf()', () => {
const userId = 1;
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
const activeCtx = {
accessToken: {userId: userId, id: 'DEFAULT_TOKEN'},
headers: {origin: 'http://localhost:5000'}
};
const ctx = {req: activeCtx};
it('should create a new PDF file and set true the hasPdf property', async() => {
const invoiceId = 1;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const response = {
pipe: () => {},
on: () => {},
data: {
pipe: () => {},
on: () => {},
}
};
spyOn(got, 'stream').and.returnValue(response);
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(response)));
spyOn(models.InvoiceContainer, 'container').and.returnValue({
client: {root: '/path'}
});
@ -32,9 +36,10 @@ describe('InvoiceOut createPdf()', () => {
const options = {transaction: tx};
try {
const result = await models.InvoiceOut.createPdf(ctx, invoiceId, options);
await models.InvoiceOut.createPdf(ctx, invoiceId, options);
const invoiceOut = await models.InvoiceOut.findById(invoiceId, null, options);
expect(result.hasPdf).toBe(true);
expect(invoiceOut.hasPdf).toBe(true);
await tx.rollback();
} catch (e) {

View File

@ -19,6 +19,10 @@ class Controller extends Section {
this.id = value.id;
}
get hasInvoicing() {
return this.aclService.hasAny(['invoicing']);
}
loadData() {
const filter = {
include: [
@ -51,8 +55,13 @@ class Controller extends Section {
deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.id}/delete`)
.then(() => this.$state.go('invoiceOut.index'))
.then(() => this.$state.reload())
.then(() => {
const isInsideInvoiceOut = this.$state.current.name.startsWith('invoiceOut');
if (isInsideInvoiceOut)
this.$state.go('invoiceOut.index');
else
this.$state.reload();
})
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
}

View File

@ -50,6 +50,35 @@ describe('vnInvoiceOutDescriptorMenu', () => {
});
});
describe('deleteInvoiceOut()', () => {
it(`should make a query and call showSuccess()`, () => {
controller.invoiceOut = invoiceOut;
controller.$state.reload = jest.fn();
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/delete`).respond();
controller.deleteInvoiceOut();
$httpBackend.flush();
expect(controller.$state.reload).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
it(`should make a query and call showSuccess() after state.go if the state wasn't in invoiceOut module`, () => {
controller.invoiceOut = invoiceOut;
jest.spyOn(controller.$state, 'go').mockReturnValue('ok');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.current.name = 'invoiceOut.card.something';
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/delete`).respond();
controller.deleteInvoiceOut();
$httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('invoiceOut.index');
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('sendPdfInvoice()', () => {
it('should make a query to the email invoice endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');

View File

@ -1,8 +1,8 @@
<vn-crud-model
vn-id="model"
url="SalesMonitors/salesFilter"
limit="20"
order="shipped DESC, theoreticalHour, id">
auto-load="false"
limit="20">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar

View File

@ -149,7 +149,10 @@ class Controller extends Section {
return this.$http.post(`Tickets/${this.id}/setDeleted`)
.then(() => this.reload())
.then(() => {
this.$state.go('ticket.index');
const isInsideTicket = this.$state.current.name.startsWith('ticket');
if (isInsideTicket)
this.$state.go('ticket.index');
this.vnApp.showSuccess(this.$t('Ticket deleted. You can undo this action within the first hour'));
});
}

View File

@ -78,9 +78,22 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
describe('deleteTicket()', () => {
it('should make a query and call vnApp.showSuccess()', () => {
jest.spyOn(controller, 'reload').mockReturnThis();
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPOST(`Tickets/${ticket.id}/setDeleted`).respond();
controller.deleteTicket();
$httpBackend.flush();
expect(controller.reload).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
it(`should make a query and call showSuccess() after state.go if the state wasn't inside ticket module`, () => {
jest.spyOn(controller, 'reload').mockReturnThis();
jest.spyOn(controller.$state, 'go').mockReturnValue('ok');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.current.name = 'ticket.card.something';
$httpBackend.expectPOST(`Tickets/${ticket.id}/setDeleted`).respond();
controller.deleteTicket();

View File

@ -12,6 +12,7 @@
"node": ">=14"
},
"dependencies": {
"axios": "^0.25.0",
"bmp-js": "^0.1.0",
"compression": "^1.7.3",
"fs-extra": "^5.0.0",
@ -42,6 +43,7 @@
"strong-error-handler": "^2.3.2",
"uuid": "^3.3.3",
"vn-loopback": "file:./loopback",
"vn-print": "file:./print",
"xml2js": "^0.4.23"
},
"devDependencies": {

View File

@ -1,11 +1,9 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const puppeteer = require('puppeteer');
const templatesPath = path.resolve(__dirname, './templates');
const componentsPath = path.resolve(__dirname, './core/components');
const config = require('./core/config');
module.exports = async app => {
global.appPath = __dirname;
@ -53,21 +51,4 @@ module.exports = async app => {
app.use(`/api/${templateName}/assets`, express.static(assetsDir));
});
});
// Instantiate Puppeteer browser
async function launchBrowser() {
config.browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--single-process',
'--no-zygote'
]
});
config.browser.on('disconnected', launchBrowser);
}
launchBrowser();
};

View File

@ -6,6 +6,8 @@
.grid {
font-family: Arial, Helvetica, sans-serif;
font-size: 16px !important;
max-width: 1200px;
margin: 0 auto;
width: 100%
}

View File

@ -48,6 +48,12 @@
"pool": true
},
"storage": {
"root": "./storage/dms"
"root": "./storage/dms",
"invoice": {
"root": "./storage/pdfs/invoice"
},
"signature": {
"root": "./storage/signatures"
}
}
}

View File

@ -27,29 +27,50 @@ class Component {
get locale() {
if (!this._locale)
this.getLocale();
this._locale = this.getLocales();
return this._locale;
}
getLocale() {
const mergedLocale = {messages: {}};
getLocales() {
const mergedLocales = {messages: {}};
const localePath = path.resolve(__dirname, `${this.path}/locale`);
if (!fs.existsSync(localePath))
return mergedLocale;
return mergedLocales;
const localeDir = fs.readdirSync(localePath);
localeDir.forEach(locale => {
for (const locale of localeDir) {
const fullPath = path.join(localePath, '/', locale);
const yamlLocale = fs.readFileSync(fullPath, 'utf8');
const jsonLocale = yaml.safeLoad(yamlLocale);
const localeName = locale.replace('.yml', '');
mergedLocale.messages[localeName] = jsonLocale;
});
mergedLocales.messages[localeName] = jsonLocale;
}
this._locale = mergedLocale;
return mergedLocales;
}
async getUserLocale() {
let locale = this.args.auth.locale;
// Fetches user locale from mixing method getLocale()
if (this.args.recipientId) {
const component = await this.component();
locale = await component.getLocale(this.args.recipientId);
}
const messages = this.locale.messages;
const userTranslations = messages[locale];
if (!userTranslations) {
const fallbackLocale = config.i18n.fallbackLocale;
return messages[fallbackLocale];
}
return userTranslations;
}
get stylesheet() {
@ -75,7 +96,7 @@ class Component {
build() {
const fullPath = path.resolve(__dirname, this.path);
if (!fs.existsSync(fullPath))
throw new Error(`Sample "${this.name}" not found`);
throw new Error(`Template "${this.name}" not found`);
const component = require(`${this.path}/${this.name}`);
component.i18n = this.locale;

View File

@ -1,5 +1,9 @@
<header>
<img v-bind:src="getReportSrc('report-logo.svg')" alt="Verdnatura"/>
<img
v-if="companyGroup == 'verdnatura'"
v-bind:src="getReportSrc('report-logo.svg')"
alt="Verdnatura"
/>
<section>
{{companyName}}. {{company.street}}.
{{company.postCode}} {{company.city}}.

View File

@ -10,9 +10,16 @@ module.exports = {
},
computed: {
companyName() {
if (!this.company.name) return;
if (this.company.name)
return this.company.name.toUpperCase();
return this.company.name.toUpperCase();
return;
},
companyGroup() {
if (this.company.groupName)
return this.company.groupName.toLowerCase();
return;
},
companyPhone() {
if (!this.company.phone) return;
@ -30,8 +37,15 @@ module.exports = {
methods: {
getCompany(code) {
return db.findOne(`
SELECT s.name, s.street, s.postCode, s.city, s.phone
SELECT
s.name,
s.street,
s.postCode,
s.city,
s.phone,
cg.code AS groupName
FROM company c
JOIN companyGroup cg ON cg.id = c.companyGroupFk
JOIN supplier s ON s.id = c.id
WHERE c.code = ?`, [code]);
},

View File

@ -19,22 +19,7 @@ class Email extends Component {
}
async getSubject() {
const component = await this.component();
let locale = this.args.auth.locale;
if (this.args.recipientId)
locale = await component.getLocale(this.args.recipientId);
const messages = this.locale.messages;
const userTranslations = messages[locale];
if (!userTranslations) {
const fallbackLocale = config.i18n.fallbackLocale;
return messages[fallbackLocale].subject;
}
return userTranslations.subject;
return (await this.getUserLocale())['subject'];
}
/**
@ -63,6 +48,7 @@ class Email extends Component {
const reportName = fileName.replace('.pdf', '');
const report = new Report(reportName, this.args);
fileCopy.content = await report.toPdfStream();
fileCopy.filename = await report.getFileName();
}
attachments.push(fileCopy);

View File

@ -2,6 +2,7 @@ const fs = require('fs');
const path = require('path');
const config = require('./config');
const Component = require('./component');
const puppeteer = require('puppeteer');
if (!process.env.OPENSSL_CONF)
process.env.OPENSSL_CONF = '/etc/ssl/';
@ -17,6 +18,10 @@ class Report extends Component {
return `../templates/reports/${this.name}`;
}
async getName() {
return (await this.getUserLocale())['reportName'];
}
async toPdfStream() {
const template = await this.render();
const defaultOptions = Object.assign({}, config.pdf);
@ -27,7 +32,17 @@ class Report extends Component {
if (fs.existsSync(fullPath))
options = require(optionsPath);
const page = (await config.browser.pages())[0];
const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--single-process',
'--no-zygote'
]
});
const page = (await browser.pages())[0];
await page.emulateMedia('screen');
await page.setContent(template);
@ -47,8 +62,43 @@ class Report extends Component {
const buffer = await page.pdf(options);
await browser.close();
return buffer;
}
/**
* Returns all the params that ends with id
*
* @return {array} List of identifiers
*/
getIdentifiers() {
const identifiers = [];
const args = this.args;
const keys = Object.keys(args);
for (let arg of keys) {
if (arg.endsWith('Id'))
identifiers.push(arg);
}
return identifiers;
}
async getFileName() {
const args = this.args;
const identifiers = this.getIdentifiers(args);
const name = await this.getName();
const params = [];
params.push(name);
for (let id of identifiers)
params.push(args[id]);
const fileName = params.join('_');
return `${fileName}.pdf`;
}
}
module.exports = Report;

View File

@ -1,43 +1,30 @@
const path = require('path');
const fs = require('fs');
const db = require('./database');
module.exports = app => {
const methodsPath = path.resolve(__dirname, '../methods');
const methodsDir = fs.readdirSync(methodsPath);
const methods = [];
const routes = require('../methods/routes');
// Get all methods
for (let method of methodsDir) {
if (method.includes('.js'))
methods.push(method.replace('.js', ''));
}
// Auth middleware
const paths = [];
for (let method of methods)
paths.push(`/api/${method}/*`);
app.use(paths, async function(req, res, next) {
const token = getToken(req);
const query = `SELECT at.id, at.userId, eu.email, u.lang, at.ttl, at.created
FROM salix.AccessToken at
JOIN account.user u ON u.id = at.userid
JOIN account.emailUser eu ON eu.userFk = u.id
WHERE at.id = ?`;
const paths = routes.map(route => route.url);
app.use(paths, async function(request, response, next) {
try {
const token = getToken(request);
const query = `SELECT at.id, at.userId, eu.email, u.lang, at.ttl, at.created
FROM salix.AccessToken at
JOIN account.user u ON u.id = at.userid
JOIN account.emailUser eu ON eu.userFk = u.id
WHERE at.id = ?`;
const auth = await db.findOne(query, [token]);
if (!auth || isTokenExpired(auth.created, auth.ttl))
throw new Error('Invalid authorization token');
const args = Object.assign({}, req.query);
const props = Object.assign(args, req.body);
const args = Object.assign({}, request.query);
const props = Object.assign(args, request.body);
props.authorization = auth.id;
req.args = props;
req.args.auth = {
response.locals = props;
response.locals.auth = {
userId: auth.userId,
token: auth.id,
email: auth.email,
@ -50,6 +37,10 @@ module.exports = app => {
}
});
// Register routes
for (let route of routes)
app.use(route.url, route.cb);
function getToken(request) {
const headers = request.headers;
const queryParams = request.query;
@ -68,8 +59,4 @@ module.exports = app => {
return false;
}
// Mount methods
for (let method of methods)
require(`../methods/${method}`)(app);
};

View File

@ -25,14 +25,17 @@ module.exports = {
throw err;
}).finally(async() => {
const attachments = [];
for (let attachment of options.attachments) {
const fileName = attachment.filename;
const filePath = attachment.path;
if (fileName.includes('.png')) return;
if (options.attachments) {
for (let attachment of options.attachments) {
const fileName = attachment.filename;
const filePath = attachment.path;
if (fileName.includes('.png')) return;
if (fileName || filePath)
attachments.push(filePath ? filePath : fileName);
if (fileName || filePath)
attachments.push(filePath ? filePath : fileName);
}
}
const fileNames = attachments.join(',\n');
await db.rawSql(`
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)

28
print/core/storage.js Normal file
View File

@ -0,0 +1,28 @@
const config = require('./config.js');
const path = require('path');
const fs = require('fs-extra');
module.exports = {
async write(stream, options) {
const storage = config.storage[options.type];
if (!storage) return;
const src = path.join(storage.root, options.path);
const fileSrc = path.join(src, options.fileName);
await fs.mkdir(src, {recursive: true});
const writeStream = fs.createWriteStream(fileSrc);
writeStream.on('open', () => writeStream.write(stream));
writeStream.on('finish', () => writeStream.end());
return new Promise(resolve => {
writeStream.on('close', () => resolve());
});
},
load(type, data) {
}
};

View File

@ -7,9 +7,8 @@ class Stylesheet {
}
mergeStyles() {
this.files.forEach(file => {
for (const file of this.files)
this.css.push(fs.readFileSync(file));
});
return this.css.join('\n');
}

View File

@ -1,311 +0,0 @@
const db = require('../core/database');
const Email = require('../core/email');
const Report = require('../core/report');
const smtp = require('../core/smtp');
const config = require('../core/config');
module.exports = app => {
app.get('/api/closure/all', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.to)
throw new Error('The argument to is required');
res.status(200).json({
message: 'Task executed successfully'
});
const tickets = await db.rawSql(`
SELECT
t.id
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.to, reqArgs.to]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, req.args);
await db.rawSql(`
UPDATE ticket t
JOIN ticketState ts ON t.id = ts.ticketFk
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
JOIN zone z ON z.id = t.zoneFk
SET t.routeFk = NULL
WHERE DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND al.code NOT IN('DELIVERED','PACKED')
AND t.routeFk
AND z.name LIKE '%MADRID%'`, [reqArgs.to, reqArgs.to]);
} catch (error) {
next(error);
}
});
app.get('/api/closure/by-ticket', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
res.status(200).json({
message: 'Task executed successfully'
});
const tickets = await db.rawSql(`
SELECT
t.id
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.id = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.ticketId]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
} catch (error) {
next(error);
}
});
app.get('/api/closure/by-agency', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.agencyModeId)
throw new Error('The argument agencyModeId is required');
if (!reqArgs.warehouseId)
throw new Error('The argument warehouseId is required');
if (!reqArgs.to)
throw new Error('The argument to is required');
res.status(200).json({
message: 'Task executed successfully'
});
const agenciesId = reqArgs.agencyModeId.split(',');
const tickets = await db.rawSql(`
SELECT
t.id
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.agencyModeFk IN(?)
AND t.warehouseFk = ?
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [
agenciesId,
reqArgs.warehouseId,
reqArgs.to,
reqArgs.to
]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
} catch (error) {
next(error);
}
});
app.get('/api/closure/by-route', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.routeId)
throw new Error('The argument routeId is required');
res.status(200).json({
message: 'Task executed successfully'
});
const tickets = await db.rawSql(`
SELECT
t.id
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.routeFk = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.routeId]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
// Send route report to the agency
const agencyMail = await db.findValue(`
SELECT am.reportMail
FROM route r
JOIN agencyMode am ON am.id = r.agencyModeFk
WHERE r.id = ?`, [reqArgs.routeId]);
if (agencyMail) {
const args = Object.assign({
routeId: reqArgs.routeId,
recipient: agencyMail
}, reqArgs);
const email = new Email('driver-route', args);
await email.send();
}
} catch (error) {
next(error);
}
});
async function closeAll(ticketIds, reqArgs) {
const failedtickets = [];
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM ticket t
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE t.id IN(?)`, [ticketIds]);
for (const ticket of tickets) {
try {
await db.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id]);
if (!ticket.salesPersonFk || !ticket.isToBeMailed) continue;
if (!ticket.recipient) {
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk}</strong> porque no tiene un email especificado.<br/><br/>
Para dejar de recibir esta notificación, asígnale un email o desactiva
la notificación por email para este cliente.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
continue;
}
const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice;
if (hasToInvoice) {
const invoice = await db.findOne(`
SELECT io.id, io.ref, io.serial, cny.code companyCode
FROM ticket t
JOIN invoiceOut io ON io.ref = t.refFk
JOIN company cny ON cny.id = io.companyFk
WHERE t.id = ?
`, [ticket.id]);
const args = Object.assign({
invoiceId: invoice.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
let mailOptions = {};
if (invoice.serial == 'E' && invoice.companyCode == 'VNL') {
const exportation = new Report('exportation', args);
const stream = await exportation.toPdfStream();
const fileName = `exportation-${invoice.ref}.pdf`;
mailOptions = {
overrideAttachments: false,
attachments: [{
filename: fileName,
content: stream
}]
};
}
const email = new Email('invoice', args);
await email.send(mailOptions);
} else {
const args = Object.assign({
ticketId: ticket.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const email = new Email('delivery-note-link', args);
await email.send();
}
} catch (error) {
// Domain not found
if (error.responseCode == 450)
return invalidEmail(ticket);
// Save tickets on a list of failed ids
failedtickets.push({
id: ticket.id,
stacktrace: error
});
}
}
// Send email with failed tickets
if (failedtickets.length > 0) {
let body = 'This following tickets have failed:<br/><br/>';
for (ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong>
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
}
smtp.send({
to: config.app.reportEmail,
subject: '[API] Nightly ticket closure report',
html: body
});
}
}
async function invalidEmail(ticket) {
await db.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
ticket.clientFk
]);
const oldInstance = `{"email": "${ticket.recipient}"}`;
const newInstance = `{"email": ""}`;
await db.rawSql(`
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
ticket.clientFk,
oldInstance,
newInstance
]);
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta o no está disponible.<br/><br/>
Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente.
Actualiza la dirección de email con una correcta.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
}
};

View File

@ -0,0 +1,58 @@
const db = require('vn-print/core/database');
const closure = require('./closure');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.to)
throw new Error('The argument to is required');
response.status(200).json({
message: 'Success'
});
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM ticket t
JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY t.id`, [reqArgs.to, reqArgs.to]);
await closure.start(tickets, response.locals);
await db.rawSql(`
UPDATE ticket t
JOIN ticketState ts ON t.id = ts.ticketFk
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
JOIN zone z ON z.id = t.zoneFk
SET t.routeFk = NULL
WHERE DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND al.code NOT IN('DELIVERED','PACKED')
AND t.routeFk
AND z.name LIKE '%MADRID%'`, [reqArgs.to, reqArgs.to]);
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,58 @@
const db = require('vn-print/core/database');
const closure = require('./closure');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.agencyModeId)
throw new Error('The argument agencyModeId is required');
if (!reqArgs.warehouseId)
throw new Error('The argument warehouseId is required');
if (!reqArgs.to)
throw new Error('The argument to is required');
response.status(200).json({
message: 'Success'
});
const agencyIds = reqArgs.agencyModeId.split(',');
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND t.agencyModeFk IN(?)
AND t.warehouseFk = ?
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [
agencyIds,
reqArgs.warehouseId,
reqArgs.to,
reqArgs.to
]);
await closure.start(tickets, response.locals);
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,61 @@
const db = require('vn-print/core/database');
const Email = require('vn-print/core/email');
const closure = require('./closure');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.routeId)
throw new Error('The argument routeId is required');
response.status(200).json({
message: 'Success'
});
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND t.routeFk = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.routeId]);
await closure.start(tickets, response.locals);
// Send route report to the agency
const agencyMail = await db.findValue(`
SELECT am.reportMail
FROM route r
JOIN agencyMode am ON am.id = r.agencyModeFk
WHERE r.id = ?`, [reqArgs.routeId]);
if (agencyMail) {
const args = Object.assign({
routeId: Number.parseInt(reqArgs.routeId),
recipient: agencyMail
}, response.locals);
const email = new Email('driver-route', args);
await email.send();
}
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,43 @@
const db = require('vn-print/core/database');
const closure = require('./closure');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
response.status(200).json({
message: 'Success'
});
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND t.id = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.ticketId]);
await closure.start(tickets, response.locals);
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,153 @@
const db = require('vn-print/core/database');
const Report = require('vn-print/core/report');
const Email = require('vn-print/core/email');
const smtp = require('vn-print/core/smtp');
const config = require('vn-print/core/config');
const storage = require('vn-print/core/storage');
module.exports = {
async start(tickets, reqArgs) {
if (tickets.length == 0) return;
const failedtickets = [];
for (const ticket of tickets) {
try {
await db.rawSql('START TRANSACTION');
await db.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id]);
const invoiceOut = await db.findOne(`
SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
FROM ticket t
JOIN invoiceOut io ON io.ref = t.refFk
JOIN company cny ON cny.id = io.companyFk
WHERE t.id = ?
`, [ticket.id]);
const mailOptions = {
overrideAttachments: true,
attachments: []
};
const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
if (invoiceOut) {
const args = Object.assign({
invoiceId: invoiceOut.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const invoiceReport = new Report('invoice', args);
const stream = await invoiceReport.toPdfStream();
const issued = invoiceOut.issued;
const year = issued.getFullYear().toString();
const month = (issued.getMonth() + 1).toString();
const day = issued.getDate().toString();
const fileName = `${year}${invoiceOut.ref}.pdf`;
// Store invoice
storage.write(stream, {
type: 'invoice',
path: `${year}/${month}/${day}`,
fileName: fileName
});
await db.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id]);
if (isToBeMailed) {
const invoiceAttachment = {
filename: fileName,
content: stream
};
if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
const exportation = new Report('exportation', args);
const stream = await exportation.toPdfStream();
const fileName = `CITES-${invoiceOut.ref}.pdf`;
mailOptions.attachments.push({
filename: fileName,
content: stream
});
}
mailOptions.attachments.push(invoiceAttachment);
const email = new Email('invoice', args);
await email.send(mailOptions);
}
} else if (isToBeMailed) {
const args = Object.assign({
ticketId: ticket.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const email = new Email('delivery-note-link', args);
await email.send();
}
await db.rawSql('COMMIT');
} catch (error) {
await db.rawSql('ROLLBACK');
// Domain not found
if (error.responseCode == 450)
return invalidEmail(ticket);
// Save tickets on a list of failed ids
failedtickets.push({
id: ticket.id,
stacktrace: error
});
}
}
// Send email with failed tickets
if (failedtickets.length > 0) {
let body = 'This following tickets have failed:<br/><br/>';
for (const ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong>
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
}
smtp.send({
to: config.app.reportEmail,
subject: '[API] Nightly ticket closure report',
html: body
});
}
},
async invalidEmail(ticket) {
await db.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
ticket.clientFk
]);
const oldInstance = `{"email": "${ticket.recipient}"}`;
const newInstance = `{"email": ""}`;
await db.rawSql(`
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
ticket.clientFk,
oldInstance,
newInstance
]);
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta o no está disponible.<br/><br/>
Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente.
Actualiza la dirección de email con una correcta.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
}
};

View File

@ -0,0 +1,9 @@
const express = require('express');
const router = new express.Router();
router.get('/all', require('./closeAll'));
router.get('/by-ticket', require('./closeByTicket'));
router.get('/by-agency', require('./closeByAgency'));
router.get('/by-route', require('./closeByRoute'));
module.exports = router;

View File

@ -1,31 +0,0 @@
module.exports = app => {
app.use('/api/csv/delivery-note', require('./csv/delivery-note')(app));
app.use('/api/csv/invoice', require('./csv/invoice')(app));
app.toCSV = function toCSV(rows) {
const [columns] = rows;
let content = Object.keys(columns).join('\t');
for (let row of rows) {
const values = Object.values(row);
const finalValues = values.map(value => {
if (value instanceof Date) return formatDate(value);
if (value === null) return '';
return value;
});
content += '\n';
content += finalValues.join('\t');
}
return content;
};
function formatDate(date) {
return new Intl.DateTimeFormat('es', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
}
};

31
print/methods/csv/csv.js Normal file
View File

@ -0,0 +1,31 @@
function toCSV(rows) {
const [columns] = rows;
let content = Object.keys(columns).join('\t');
for (let row of rows) {
const values = Object.values(row);
const finalValues = values.map(value => {
if (value instanceof Date) return formatDate(value);
if (value === null) return '';
return value;
});
content += '\n';
content += finalValues.join('\t');
}
return content;
}
function formatDate(date) {
return new Intl.DateTimeFormat('es', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
}
module.exports = {
toCSV,
formatDate
};

View File

@ -0,0 +1,24 @@
const path = require('path');
const db = require('vn-print/core/database');
const {toCSV} = require('../csv');
const sqlPath = path.join(__dirname, 'sql');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const content = toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
response.setHeader('Content-type', 'text/csv');
response.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
response.end(content);
} catch (error) {
next(error);
}
};

View File

@ -1,82 +0,0 @@
const express = require('express');
const router = new express.Router();
const path = require('path');
const db = require('../../../core/database');
const sqlPath = path.join(__dirname, 'sql');
module.exports = app => {
router.get('/preview', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
res.setHeader('Content-type', 'application/json; charset=utf-8');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
router.get('/download', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
res.setHeader('Content-type', 'text/csv');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
const Email = require('../../../core/email');
router.get('/send', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const ticket = await db.findOneFromDef(`${sqlPath}/ticket`, [ticketId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const args = Object.assign({
ticketId: (String(ticket.id)),
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
const email = new Email('delivery-note', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
res.status(200).json({message: 'ok'});
} catch (error) {
next(error);
}
});
return router;
};

View File

@ -0,0 +1,40 @@
const path = require('path');
const db = require('vn-print/core/database');
const Email = require('vn-print/core/email');
const {toCSV} = require('../csv');
const sqlPath = path.join(__dirname, 'sql');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const ticket = await db.findOneFromDef(`${sqlPath}/ticket`, [ticketId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const args = Object.assign({
ticketId: (String(ticket.id)),
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, response.locals);
const content = toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
const email = new Email('delivery-note', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
response.status(200).json({message: 'Success'});
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,9 @@
const express = require('express');
const router = new express.Router();
router.get('/delivery-note/download', require('./delivery-note/download'));
router.get('/delivery-note/send', require('./delivery-note/send'));
router.get('/invoice/download', require('./invoice/download'));
router.get('/invoice/send', require('./invoice/send'));
module.exports = router;

View File

@ -0,0 +1,24 @@
const path = require('path');
const db = require('vn-print/core/database');
const {toCSV} = require('../csv');
const sqlPath = path.join(__dirname, 'sql');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const content = toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
response.setHeader('Content-type', 'text/csv');
response.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
response.end(content);
} catch (error) {
next(error);
}
};

View File

@ -1,82 +0,0 @@
const express = require('express');
const router = new express.Router();
const path = require('path');
const db = require('../../../core/database');
const sqlPath = path.join(__dirname, 'sql');
module.exports = app => {
router.get('/preview', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
res.setHeader('Content-type', 'application/json; charset=utf-8');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
router.get('/download', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
res.setHeader('Content-type', 'text/csv');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
const Email = require('../../../core/email');
router.get('/send', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const invoice = await db.findOneFromDef(`${sqlPath}/invoice`, [invoiceId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const args = Object.assign({
invoiceId: (String(invoice.id)),
recipientId: invoice.clientFk,
recipient: invoice.recipient,
replyTo: invoice.salesPersonEmail
}, reqArgs);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
const email = new Email('invoice', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
res.status(200).json({message: 'ok'});
} catch (error) {
next(error);
}
});
return router;
};

View File

@ -0,0 +1,40 @@
const path = require('path');
const db = require('vn-print/core/database');
const Email = require('vn-print/core/email');
const {toCSV} = require('../csv');
const sqlPath = path.join(__dirname, 'sql');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const invoice = await db.findOneFromDef(`${sqlPath}/invoice`, [invoiceId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const args = Object.assign({
invoiceId: (String(invoice.id)),
recipientId: invoice.clientFk,
recipient: invoice.recipient,
replyTo: invoice.salesPersonEmail
}, response.locals);
const content = toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
const email = new Email('invoice', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
response.status(200).json({message: 'Success'});
} catch (error) {
next(error);
}
};

View File

@ -1,33 +0,0 @@
const Email = require('../core/email');
module.exports = app => {
app.get(`/api/email/:name`, async(req, res, next) => {
try {
const reportName = req.params.name;
const email = new Email(reportName, req.args);
await email.send();
res.status(200).json({
message: 'Sent'
});
} catch (e) {
next(e);
}
});
app.get(`/api/email/:name/preview`, async(req, res, next) => {
try {
const reportName = req.params.name;
const args = req.args;
args.isPreview = true;
const email = new Email(reportName, args);
const rendered = await email.render();
res.send(rendered);
} catch (e) {
next(e);
}
});
};

View File

@ -0,0 +1,16 @@
const Email = require('vn-print/core/email');
module.exports = async function(request, response, next) {
try {
const templateName = request.params.name;
const args = response.locals;
const email = new Email(templateName, args);
await email.send();
response.status(200).json({
message: 'Sent'
});
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,7 @@
const express = require('express');
const router = new express.Router();
router.get('/:name', require('./email'));
router.get('/:name/preview', require('./preview'));
module.exports = router;

View File

@ -0,0 +1,14 @@
const Email = require('vn-print/core/email');
module.exports = async function(request, response, next) {
try {
const templateName = request.params.name;
const args = Object.assign({isPreview: true}, response.locals);
const email = new Email(templateName, args);
const template = await email.render();
response.send(template);
} catch (error) {
next(error);
}
};

View File

@ -1,54 +0,0 @@
const Report = require('../core/report');
module.exports = app => {
app.get(`/api/report/:name`, async(req, res, next) => {
try {
const reportName = req.params.name;
const fileName = getFileName(reportName, req.args);
const report = new Report(reportName, req.args);
if (req.args.preview) {
const template = await report.render();
res.send(template);
} else {
const stream = await report.toPdfStream();
res.setHeader('Content-type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(stream);
}
} catch (error) {
next(error);
}
});
/**
* Returns all the params that ends with id
* @param {object} args - Params object
*
* @return {array} List of identifiers
*/
function getIdentifiers(args) {
const identifiers = [];
const keys = Object.keys(args);
for (let arg of keys) {
if (arg.endsWith('Id'))
identifiers.push(arg);
}
return identifiers;
}
function getFileName(name, args) {
const identifiers = getIdentifiers(args);
const params = [];
params.push(name);
for (let id of identifiers)
params.push(args[id]);
const fileName = params.join('_');
return `${fileName}.pdf`;
}
};

View File

@ -0,0 +1,17 @@
const Report = require('vn-print/core/report');
module.exports = async function(request, response, next) {
try {
const reportName = request.params.name;
const args = response.locals;
const report = new Report(reportName, args);
const stream = await report.toPdfStream();
const fileName = await report.getFileName();
response.setHeader('Content-type', 'application/pdf');
response.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
response.end(stream);
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,7 @@
const express = require('express');
const router = new express.Router();
router.get('/:name', require('./document'));
router.get('/:name/preview', require('./preview'));
module.exports = router;

View File

@ -0,0 +1,13 @@
const Report = require('vn-print/core/report');
module.exports = async function(request, response, next) {
try {
const reportName = request.params.name;
const report = new Report(reportName, request.query);
const template = await report.render();
response.send(template);
} catch (error) {
next(error);
}
};

18
print/methods/routes.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = [
{
url: '/api/report',
cb: require('./report')
},
{
url: '/api/email',
cb: require('./email')
},
{
url: '/api/csv',
cb: require('./csv')
},
{
url: '/api/closure',
cb: require('./closure')
},
];

View File

@ -21,6 +21,7 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
},
from: {

View File

@ -10,6 +10,7 @@ module.exports = {
},
props: {
claimId: {
type: [Number, String],
required: true
}
}

View File

@ -16,6 +16,7 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
},
from: {

View File

@ -18,6 +18,7 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
}
}

View File

@ -10,6 +10,7 @@ module.exports = {
},
props: {
ticketId: {
type: [Number, String],
required: true
}
}

View File

@ -10,7 +10,7 @@ module.exports = {
},
props: {
ticketId: {
type: String,
type: [Number, String],
required: true
}
}

View File

@ -10,7 +10,7 @@ module.exports = {
},
props: {
routeId: {
type: String,
type: [Number, String],
required: true
}
}

View File

@ -18,7 +18,7 @@ module.exports = {
},
props: {
invoiceId: {
type: String,
type: [Number, String],
required: true
}
}

View File

@ -30,9 +30,11 @@ module.exports = {
required: true
},
recipientId: {
type: [Number, String],
required: true
},
companyId: {
type: [Number, String],
required: true
}
}

View File

@ -1,5 +1,4 @@
const Component = require(`${appPath}/core/component`);
const db = require(`${appPath}/core/database`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
const attachment = new Component('attachment');
@ -28,9 +27,11 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
},
companyId: {
type: [Number, String],
required: true
},
}

View File

@ -26,6 +26,7 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
}
}

View File

@ -24,6 +24,7 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
}
}

View File

@ -16,9 +16,11 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
},
companyId: {
type: [Number, String],
required: true
}
}

View File

@ -21,6 +21,7 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
},
from: {

View File

@ -25,6 +25,7 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
},
from: {

View File

@ -1,3 +1,4 @@
reportName: consumo-cliente
title: Consumo
Client: Cliente
clientData: Datos del cliente

View File

@ -32,6 +32,7 @@ module.exports = {
},
props: {
claimId: {
type: [Number, String],
required: true
}
}

View File

@ -1,3 +1,4 @@
reportName: orden-de-recogida
title: Ord. recogida
claimId: Reclamación
clientId: Cliente

View File

@ -69,6 +69,7 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
},
from: {

View File

@ -1,3 +1,4 @@
reportName: extracto-cliente
title: Extracto
clientId: Cliente
clientData: Datos del cliente

View File

@ -1,3 +1,4 @@
reportName: releve-de-compte
title: Relevé de compte
clientId: Client
clientData: Données client

View File

@ -10,7 +10,6 @@ module.exports = {
throw new Error('Something went wrong');
this.client = await this.findOneFromDef('client', [this.ticket.clientFk]);
},
computed: {
issued: function() {
@ -23,6 +22,7 @@ module.exports = {
},
props: {
ticketId: {
type: [Number, String],
required: true
}
}

View File

@ -1,3 +1,4 @@
reportName: autorizacion-cmr
description: '<em>{socialName}</em> una sociedad debidamente constituida con responsabilidad <em>limitada</em>
y registrada conforme al derecho de sociedades de {country} y aquí representada por
<span>___________________</span>. {socialName}, con domicilio en {address},

View File

@ -1,3 +1,4 @@
reportName: solicitud-de-credito
fields:
title: Solicitud de crédito
date: Fecha

View File

@ -117,7 +117,7 @@ module.exports = {
},
props: {
ticketId: {
type: String,
type: [Number, String],
required: true
}
}

View File

@ -1,3 +1,4 @@
reportName: delivery-note
title: Delivery note
ticketId: Delivery note
clientId: Client

View File

@ -1,3 +1,4 @@
reportName: albaran
title: Albarán
ticketId: Albarán
clientId: Cliente

View File

@ -1,3 +1,4 @@
reportName: bon-de-livraison
title: Bon de livraison
ticketId: BL
clientId: Client

View File

@ -1,3 +1,4 @@
reportName: nota-de-entrega
title: Nota de Entrega
ticketId: Nota de Entrega
clientId: Cliente

View File

@ -39,6 +39,7 @@ module.exports = {
},
props: {
routeId: {
type: [Number, String],
required: true
}
}

View File

@ -1,3 +1,4 @@
reportName: hoja-de-ruta
title: Hoja de ruta
information: Información
date: Fecha

View File

@ -40,7 +40,7 @@ module.exports = {
},
props: {
entryId: {
type: String,
type: [Number, String],
required: true
}
}

View File

@ -1,3 +1,4 @@
reportName: pedido-de-entrada
title: Pedido
supplierName: Proveedor
supplierStreet: Dirección

View File

@ -28,6 +28,7 @@ module.exports = {
},
props: {
invoiceId: {
type: [Number, String],
required: true
}
}

View File

@ -1,3 +1,4 @@
reportName: carta-CITES
title: 'Carta CITES'
toAttention: 'A la atención del Sr. Administrador de la Aduana de la Farga de Moles.'
declaration: 'Por la presente DECLARO, bajo mi responsabilidad, que las mercancías detalladas en la factura

View File

@ -1,3 +1,4 @@
reportName: orden-de-carga
title: Orden de carga
reference: Referencia
information: Información

View File

@ -32,7 +32,7 @@ module.exports = {
},
props: {
invoiceId: {
type: String,
type: [Number, String],
required: true
}
}

View File

@ -1,3 +1,4 @@
reportName: factura
title: Factura
invoice: Factura
clientId: Cliente

View File

@ -115,7 +115,7 @@ module.exports = {
},
props: {
invoiceId: {
type: String,
type: [Number, String],
required: true
}
}

View File

@ -0,0 +1,36 @@
reportName: invoice
title: Invoice
invoice: Invoice
clientId: Client
invoiceData: Invoice data
fiscalId: FI / NIF
invoiceRef: Invoice {0}
deliveryNote: Delivery note
shipped: Shipped
date: Date
reference: Ref.
quantity: Qty.
concept: Concept
price: PSP/u
discount: Disc.
vat: VAT
amount: Amount
type: Type
taxBase: Tax base
tax: Tax
fee: Fee
total: Total
subtotal: Subtotal
taxBreakdown: Tax breakdown
notes: Notes
intrastat: Intrastat
code: Code
description: Description
stems: Stems
netKg: Net kg
rectifiedInvoices: Rectified invoices
issued: Issued
plantPassport: Plant passport
observations: Observations
wireTransfer: "Pay method: Transferencia"
accountNumber: "Account number: {0}"

View File

@ -1,3 +1,4 @@
reportName: factura
title: Factura
invoice: Factura
clientId: Cliente

View File

@ -63,9 +63,11 @@ module.exports = {
},
props: {
recipientId: {
type: [Number, String],
required: true
},
companyId: {
type: [Number, String],
required: true
}
}

View File

@ -1,3 +1,4 @@
reportName: extracto-de-cuenta
title: Extracto
claimId: Reclamación
clientId: Cliente

View File

@ -1,3 +1,4 @@
reportName: releve-de-compte
title: Relevé de compte
claimId: Réclamation
clientId: Client

View File

@ -1,3 +1,4 @@
reportName: receipt
title: 'Recibo'
date: 'Fecha'
payed: 'En {0}, a {1} de {2} de {3}'

View File

@ -25,6 +25,7 @@ module.exports = {
},
props: {
receiptId: {
type: [Number, String],
required: true
}
}

Some files were not shown because too many files have changed in this diff Show More