Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4090-global_invoincing
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
9bcfebcae6
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE `vn`.`zipConfig` (
|
||||
`id` double(10,2) NOT NULL,
|
||||
`maxSize` int(11) DEFAULT NULL COMMENT 'in MegaBytes',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
|
||||
|
||||
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
|
||||
VALUES
|
||||
('ZipConfig', '*', '*', 'ALLOW', 'ROLE', 'employee');
|
|
@ -0,0 +1,55 @@
|
|||
const JSZip = require('jszip');
|
||||
const fs = require('fs-extra');
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('downloadZip', {
|
||||
description: 'Download a zip file with multiple invoices pdfs',
|
||||
accessType: 'READ',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'ids',
|
||||
type: ['number'],
|
||||
description: 'The invoice ids'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
arg: 'base64',
|
||||
type: 'string',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: '/downloadZip',
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.downloadZip = async function(ctx, ids, options) {
|
||||
const models = Self.app.models;
|
||||
const myOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const zip = new JSZip();
|
||||
let totalSize = 0;
|
||||
const zipConfig = await models.ZipConfig.findOne(null, myOptions);
|
||||
for (let id of ids) {
|
||||
if (zipConfig && totalSize > zipConfig.maxSize) throw new UserError('Files are too large');
|
||||
const invoiceOutPdf = await models.InvoiceOut.download(ctx, id, myOptions);
|
||||
const fileName = extractFileName(invoiceOutPdf[2]);
|
||||
const body = invoiceOutPdf[0];
|
||||
const sizeInBytes = (await fs.promises.stat(body.path)).size;
|
||||
const sizeInMegabytes = sizeInBytes / (1024 * 1024);
|
||||
totalSize += sizeInMegabytes;
|
||||
zip.file(fileName, body);
|
||||
}
|
||||
const base64 = await zip.generateAsync({type: 'base64'});
|
||||
return base64;
|
||||
};
|
||||
|
||||
function extractFileName(str) {
|
||||
const matches = str.match(/"(.*?)"/);
|
||||
return matches ? matches[1] : str;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
describe('InvoiceOut downloadZip()', () => {
|
||||
const userId = 9;
|
||||
const invoiceIds = [1, 2];
|
||||
const ctx = {
|
||||
req: {
|
||||
|
||||
accessToken: {userId: userId},
|
||||
headers: {origin: 'http://localhost:5000'},
|
||||
}
|
||||
};
|
||||
|
||||
it('should return part of link to dowloand the zip', async() => {
|
||||
const tx = await models.Order.beginTransaction({});
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
const result = await models.InvoiceOut.downloadZip(ctx, invoiceIds, options);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an error if the size of the files is too large', async() => {
|
||||
const tx = await models.Order.beginTransaction({});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
const zipConfig = {
|
||||
maxSize: 0
|
||||
};
|
||||
await models.ZipConfig.create(zipConfig, options);
|
||||
|
||||
await models.InvoiceOut.downloadZip(ctx, invoiceIds, options);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toEqual(new UserError(`Files are too large`));
|
||||
});
|
||||
});
|
|
@ -22,5 +22,8 @@
|
|||
},
|
||||
"TaxType": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"ZipConfig": {
|
||||
"dataSource": "vn"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ module.exports = Self => {
|
|||
require('../methods/invoiceOut/summary')(Self);
|
||||
require('../methods/invoiceOut/getTickets')(Self);
|
||||
require('../methods/invoiceOut/download')(Self);
|
||||
require('../methods/invoiceOut/downloadZip')(Self);
|
||||
require('../methods/invoiceOut/delete')(Self);
|
||||
require('../methods/invoiceOut/book')(Self);
|
||||
require('../methods/invoiceOut/createPdf')(Self);
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "ZipConfig",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "zipConfig"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"id": true,
|
||||
"description": "Identifier"
|
||||
},
|
||||
"maxSize": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"acls": [{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}]
|
||||
}
|
|
@ -4,10 +4,24 @@
|
|||
<vn-data-viewer
|
||||
model="model"
|
||||
class="vn-w-lg">
|
||||
<vn-card class="vn-pa-lg">
|
||||
<vn-button
|
||||
disabled="$ctrl.totalChecked == 0"
|
||||
vn-click-stop="$ctrl.openPdf()"
|
||||
icon="cloud_download"
|
||||
title="Download PDF"
|
||||
vn-tooltip="Download PDF">
|
||||
</vn-button>
|
||||
</vn-card>
|
||||
<vn-card>
|
||||
<vn-table model="model">
|
||||
<vn-thead>
|
||||
<vn-tr>
|
||||
<vn-th shrink>
|
||||
<vn-multi-check
|
||||
model="model">
|
||||
</vn-multi-check>
|
||||
</vn-th>
|
||||
<vn-th field="ref">Reference</vn-th>
|
||||
<vn-th field="issued" expand>Issued</vn-th>
|
||||
<vn-th field="amount" number>Amount</vn-th>
|
||||
|
@ -23,6 +37,12 @@
|
|||
<a ng-repeat="invoiceOut in model.data"
|
||||
class="clickable vn-tr search-result"
|
||||
ui-sref="invoiceOut.card.summary({id: {{::invoiceOut.id}}})">
|
||||
<vn-td>
|
||||
<vn-check
|
||||
ng-model="invoiceOut.checked"
|
||||
vn-click-stop>
|
||||
</vn-check>
|
||||
</vn-td>
|
||||
<vn-td>{{::invoiceOut.ref | dashIfEmpty}}</vn-td>
|
||||
<vn-td shrink>{{::invoiceOut.issued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td number>{{::invoiceOut.amount | currency: 'EUR': 2 | dashIfEmpty}}</vn-td>
|
||||
|
@ -36,15 +56,6 @@
|
|||
<vn-td expand>{{::invoiceOut.created | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td>{{::invoiceOut.companyCode | dashIfEmpty}}</vn-td>
|
||||
<vn-td shrink>{{::invoiceOut.dued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td shrink>
|
||||
<vn-icon-button
|
||||
ng-show="invoiceOut.hasPdf"
|
||||
vn-click-stop="$ctrl.openPdf(invoiceOut.id)"
|
||||
icon="cloud_download"
|
||||
title="Download PDF"
|
||||
vn-tooltip="Download PDF">
|
||||
</vn-icon-button>
|
||||
</vn-td>
|
||||
<vn-td shrink>
|
||||
<vn-icon-button
|
||||
vn-click-stop="$ctrl.preview(invoiceOut)"
|
||||
|
|
|
@ -2,14 +2,41 @@ import ngModule from '../module';
|
|||
import Section from 'salix/components/section';
|
||||
|
||||
export default class Controller extends Section {
|
||||
get checked() {
|
||||
const rows = this.$.model.data || [];
|
||||
const checkedRows = [];
|
||||
for (let row of rows) {
|
||||
if (row.checked)
|
||||
checkedRows.push(row.id);
|
||||
}
|
||||
|
||||
return checkedRows;
|
||||
}
|
||||
|
||||
get totalChecked() {
|
||||
return this.checked.length;
|
||||
}
|
||||
|
||||
preview(invoiceOut) {
|
||||
this.selectedInvoiceOut = invoiceOut;
|
||||
this.$.summary.show();
|
||||
}
|
||||
|
||||
openPdf(id) {
|
||||
let url = `api/InvoiceOuts/${id}/download?access_token=${this.vnToken.token}`;
|
||||
window.open(url, '_blank');
|
||||
openPdf() {
|
||||
if (this.checked.length <= 1) {
|
||||
const [invoiceOutId] = this.checked;
|
||||
const url = `api/InvoiceOuts/${invoiceOutId}/download?access_token=${this.vnToken.token}`;
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
const invoiceOutIds = this.checked;
|
||||
const params = {
|
||||
ids: invoiceOutIds
|
||||
};
|
||||
this.$http.post(`InvoiceOuts/downloadZip`, params)
|
||||
.then(res => {
|
||||
location.href = 'data:application/zip;base64,' + res.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,3 +6,4 @@ Minimum: Minimo
|
|||
Maximum: Máximo
|
||||
Global invoicing: Facturación global
|
||||
Manual invoicing: Facturación manual
|
||||
Files are too large: Los archivos son demasiado grandes
|
|
@ -188,7 +188,7 @@ module.exports = Self => {
|
|||
async function createSaleComponent(saleId, value, componentId, myOptions) {
|
||||
const models = Self.app.models;
|
||||
|
||||
return models.SaleComponent.create({
|
||||
return models.SaleComponent.upsert({
|
||||
saleFk: saleId,
|
||||
value: value,
|
||||
componentFk: componentId
|
||||
|
|
|
@ -5,6 +5,13 @@
|
|||
|
||||
<vn-menu vn-id="menu">
|
||||
<vn-list>
|
||||
<vn-item
|
||||
vn-acl="administrative"
|
||||
vn-acl-action="remove"
|
||||
ng-click="transferClient.show()"
|
||||
translate>
|
||||
Transfer client
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="addTurn.show()"
|
||||
vn-acl="buyer"
|
||||
|
@ -242,6 +249,36 @@
|
|||
</tpl-buttons>
|
||||
</vn-dialog>
|
||||
|
||||
<!-- Transfer Client popup -->
|
||||
|
||||
<vn-dialog
|
||||
vn-id="transferClient"
|
||||
title="transferClient"
|
||||
size="sm"
|
||||
on-accept="$ctrl.transferClient($client)">
|
||||
<tpl-body>
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
vn-id="client"
|
||||
required="true"
|
||||
url="Clients"
|
||||
label="Client"
|
||||
show-field="name"
|
||||
value-field="id"
|
||||
search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}"
|
||||
ng-model="$ctrl.ticket.client.id"
|
||||
initial-data="$ctrl.ticket.client.id"
|
||||
order="id">
|
||||
<tpl-item>
|
||||
#{{id}} - {{::name}}
|
||||
</tpl-item>
|
||||
</vn-autocomplete>
|
||||
</tpl-body>
|
||||
<tpl-buttons>
|
||||
<button response="accept" translate>Transfer client</button>
|
||||
</tpl-buttons>
|
||||
</vn-dialog>
|
||||
|
||||
<!-- Send SMS popup -->
|
||||
<vn-ticket-sms
|
||||
vn-id="sms"
|
||||
|
|
|
@ -95,6 +95,23 @@ class Controller extends Section {
|
|||
});
|
||||
}
|
||||
|
||||
transferClient() {
|
||||
this.$http.get(`Clients/${this.ticket.client.id}`).then(client => {
|
||||
const ticket = this.ticket;
|
||||
|
||||
const params =
|
||||
{
|
||||
clientFk: client.data.id,
|
||||
addressFk: client.data.defaultAddressFk,
|
||||
};
|
||||
|
||||
this.$http.patch(`Tickets/${ticket.id}`, params).then(() => {
|
||||
this.vnApp.showSuccess(this.$t('Data saved!'));
|
||||
this.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isTicketEditable() {
|
||||
if (!this.ticket) return;
|
||||
|
||||
|
|
|
@ -281,4 +281,17 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
|
|||
$httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transferClient()', () => {
|
||||
it(`should perform two queries, a get to obtain the clientData and a patch to update the ticket`, () => {
|
||||
const client =
|
||||
{
|
||||
clientFk: 1101,
|
||||
addressFk: 1,
|
||||
};
|
||||
$httpBackend.expect('GET', `Clients/${ticket.client.id}`).respond(client);
|
||||
$httpBackend.expect('PATCH', `Tickets/${ticket.id}`).respond();
|
||||
controller.transferClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,4 +9,5 @@ Send CSV Delivery Note: Enviar albarán en CSV
|
|||
Send PDF Delivery Note: Enviar albarán en PDF
|
||||
Show Proforma: Ver proforma
|
||||
Refund all: Abonar todo
|
||||
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
|
||||
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
|
||||
Transfer client: Transferir cliente
|
|
@ -22,4 +22,4 @@ SMS Pending payment: 'SMS Pago pendiente'
|
|||
Restore ticket: Restaurar ticket
|
||||
You are going to restore this ticket: Vas a restaurar este ticket
|
||||
Are you sure you want to restore this ticket?: ¿Seguro que quieres restaurar el ticket?
|
||||
Are you sure you want to refund all?: ¿Seguro que quieres abonar todo?
|
||||
Are you sure you want to refund all?: ¿Seguro que quieres abonar todo?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "salix-back",
|
||||
"version": "8.8.0",
|
||||
"version": "9.0.0",
|
||||
"author": "Verdnatura Levante SL",
|
||||
"description": "Salix backend",
|
||||
"license": "GPL-3.0",
|
||||
|
@ -39,7 +39,7 @@
|
|||
"node-ssh": "^11.0.0",
|
||||
"object-diff": "0.0.4",
|
||||
"object.pick": "^1.3.0",
|
||||
"puppeteer": "^19.0.0",
|
||||
"puppeteer": "^18.0.5",
|
||||
"read-chunk": "^3.2.0",
|
||||
"require-yaml": "0.0.1",
|
||||
"sharp": "^0.31.0",
|
||||
|
|
|
@ -9,6 +9,7 @@ module.exports = {
|
|||
concurrency: Cluster.CONCURRENCY_CONTEXT,
|
||||
maxConcurrency: cpus().length,
|
||||
puppeteerOptions: {
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
|
|
Loading…
Reference in New Issue