Merge pull request 'fixes #5092 Listado tickets sin facturar' (!1368) from 5092-listado-ticketsSinFacturar into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #1368
Reviewed-by: Javi Gallego <jgallego@verdnatura.es>
This commit is contained in:
Alexandre Riera 2023-04-04 07:17:15 +00:00
commit 25df36eec9
17 changed files with 506 additions and 11 deletions

View File

@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2314.01] - 2023-04-20
### Added
-
- (Facturas recibidas -> Bases negativas) Nueva sección
### Changed
-

View File

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

View File

@ -0,0 +1,29 @@
import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn negative bases path', () => {
let browser;
let page;
const httpRequests = [];
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
page.on('request', req => {
if (req.url().includes(`InvoiceIns/negativeBases`))
httpRequests.push(req.url());
});
await page.loginAndModule('administrative', 'invoiceIn');
await page.accessToSection('invoiceIn.negative-bases');
});
afterAll(async() => {
await browser.close();
});
it('should show negative bases in a date range', async() => {
const request = httpRequests.find(req =>
req.includes(`from`) && req.includes(`to`));
expect(request).toBeDefined();
});
});

View File

@ -271,5 +271,6 @@
"This locker has already been assigned": "Esta taquilla ya ha sido asignada",
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}",
"Not exist this branch": "La rama no existe",
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado"
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
"Insert a date range": "Inserte un rango de fechas"
}

View File

@ -0,0 +1,112 @@
const UserError = require('vn-loopback/util/user-error');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethodCtx('negativeBases', {
description: 'Find all negative bases',
accessType: 'READ',
accepts: [
{
arg: 'from',
type: 'date',
description: 'From date'
},
{
arg: 'to',
type: 'date',
description: 'To date'
},
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
},
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/negativeBases`,
verb: 'GET'
}
});
Self.negativeBases = async(ctx, options) => {
const conn = Self.dataSource.connector;
const args = ctx.args;
if (!args.from || !args.to)
throw new UserError(`Insert a date range`);
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const stmts = [];
let stmt;
stmts.push(`DROP TEMPORARY TABLE IF EXISTS tmp.ticket`);
stmts.push(new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.ticket
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticket t
WHERE shipped BETWEEN ? AND ?
AND refFk IS NULL`, [args.from, args.to]));
stmts.push(`CALL vn.ticket_getTax(NULL)`);
stmts.push(`DROP TEMPORARY TABLE IF EXISTS tmp.filter`);
stmts.push(new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.filter
ENGINE = MEMORY
SELECT
co.code company,
cou.country,
c.id clientId,
c.socialName clientSocialName,
SUM(s.quantity * s.price * ( 100 - s.discount ) / 100) amount,
negativeBase.taxableBase,
negativeBase.ticketFk,
c.isActive,
c.hasToInvoice,
c.isTaxDataChecked,
w.id comercialId,
CONCAT(w.firstName, ' ', w.lastName) comercialName
FROM vn.ticket t
JOIN vn.company co ON co.id = t.companyFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.client c ON c.id = t.clientFk
JOIN vn.country cou ON cou.id = c.countryFk
LEFT JOIN vn.worker w ON w.id = c.salesPersonFk
LEFT JOIN (
SELECT ticketFk, taxableBase
FROM tmp.ticketAmount
GROUP BY ticketFk
HAVING taxableBase < 0
) negativeBase ON negativeBase.ticketFk = t.id
WHERE t.shipped BETWEEN ? AND ?
AND t.refFk IS NULL
AND c.typeFk IN ('normal','trust')
GROUP BY t.clientFk, negativeBase.taxableBase
HAVING amount <> 0`, [args.from, args.to]));
stmt = new ParameterizedSQL(`
SELECT f.*
FROM tmp.filter f`);
stmt.merge(conn.makeWhere(args.filter.where));
stmt.merge(conn.makeOrderBy(args.filter.order));
const negativeBasesIndex = stmts.push(stmt) - 1;
stmts.push(`DROP TEMPORARY TABLE tmp.filter, tmp.ticket, tmp.ticketTax, tmp.ticketAmount`);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return negativeBasesIndex === 0 ? result : result[negativeBasesIndex];
};
};

View File

@ -0,0 +1,53 @@
const {toCSV} = require('vn-loopback/util/csv');
module.exports = Self => {
Self.remoteMethodCtx('negativeBasesCsv', {
description: 'Returns the negative bases as .csv',
accessType: 'READ',
accepts: [{
arg: 'negativeBases',
type: ['object'],
required: true
},
{
arg: 'from',
type: 'date',
description: 'From date'
},
{
arg: 'to',
type: 'date',
description: 'To date'
}],
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: '/negativeBasesCsv',
verb: 'GET'
}
});
Self.negativeBasesCsv = async ctx => {
const args = ctx.args;
const content = toCSV(args.negativeBases);
return [
content,
'text/csv',
`attachment; filename="negative-bases-${new Date(args.from).toLocaleDateString()}-${new Date(args.to).toLocaleDateString()}.csv"`
];
};
};

View File

@ -0,0 +1,47 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn negativeBases()', () => {
it('should return all negative bases in a date range', async() => {
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
const ctx = {
args: {
from: new Date().setMonth(new Date().getMonth() - 12),
to: new Date(),
filter: {}
}
};
try {
const result = await models.InvoiceIn.negativeBases(ctx, options);
expect(result.length).toBeGreaterThan(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw an error if a date range is not in args', async() => {
let error;
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
const ctx = {
args: {
filter: {}
}
};
try {
await models.InvoiceIn.negativeBases(ctx, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`Insert a date range`);
});
});

View File

@ -7,4 +7,6 @@ module.exports = Self => {
require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self);
require('../methods/invoice-in/getSerial')(Self);
require('../methods/invoice-in/negativeBases')(Self);
require('../methods/invoice-in/negativeBasesCsv')(Self);
};

View File

@ -15,3 +15,4 @@ import './create';
import './log';
import './serial';
import './serial-search-panel';
import './negative-bases';

View File

@ -24,3 +24,4 @@ Show agricultural receipt as PDF: Ver recibo agrícola como PDF
Send agricultural receipt as PDF: Enviar recibo agrícola como PDF
New InvoiceIn: Nueva Factura
Days ago: Últimos días
Negative bases: Bases negativas

View File

@ -0,0 +1,133 @@
<vn-crud-model
vn-id="model"
url="InvoiceIns/negativeBases"
auto-load="true"
params="$ctrl.params">
</vn-crud-model>
<vn-portal slot="topbar">
</vn-portal>
<vn-card>
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions>
<vn-date-picker
vn-one
label="From"
ng-model="$ctrl.params.from"
on-change="model.refresh()">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="$ctrl.params.to"
on-change="model.refresh()">
</vn-date-picker>
<vn-button
disabled="model._orgData.length == 0"
icon="download"
ng-click="$ctrl.downloadCSV()"
vn-tooltip="Download as CSV">
</vn-button>
</slot-actions>
<slot-table>
<table>
<thead>
<tr>
<th field="company">
<span translate>Company</span>
</th>
<th field="country">
<span translate>Country</span>
</th>
<th field="clientId">
<span translate>Id Client</span>
</th>
<th field="clientSocialName">
<span translate>Client</span>
</th>
<th field="amount">
<span translate>Amount</span>
</th>
<th field="taxableBase">
<span translate>Base</span>
</th>
<th field="ticketFk">
<span translate>Id Ticket</span>
</th>
<th field="isActive">
<span translate>Active</span>
</th>
<th field="hasToInvoice">
<span translate>Has To Invoice</span>
</th>
<th field="isTaxDataChecked">
<span translate>Verified data</span>
</th>
<th field="comercialName">
<span translate>Comercial</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="client in model.data">
<td>{{client.company | dashIfEmpty}}</td>
<td>{{client.country | dashIfEmpty}}</td>
<td>
<vn-span
class="link"
ng-click="clientDescriptor.show($event, client.clientId)">
{{::client.clientId | dashIfEmpty}}
</vn-span>
</td>
<td>{{client.clientSocialName | dashIfEmpty}}</td>
<td>{{client.amount | currency: 'EUR':2 | dashIfEmpty}}</td>
<td>{{client.taxableBase | dashIfEmpty}}</td>
<td>
<vn-span
class="link"
ng-click="ticketDescriptor.show($event, client.ticketFk)">
{{::client.ticketFk | dashIfEmpty}}
</vn-span>
</td>
<td>
<vn-check
disabled="true"
ng-model="client.isActive">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="client.hasToInvoice">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="client.isTaxDataChecked">
</vn-check>
</td>
<td>
<vn-span
class="link"
ng-click="workerDescriptor.show($event, client.comercialId)">
{{::client.comercialName | dashIfEmpty}}
</vn-span>
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<vn-ticket-descriptor-popover
vn-id="ticket-descriptor">
</vn-ticket-descriptor-popover>
<vn-client-descriptor-popover
vn-id="client-descriptor">
</vn-client-descriptor-popover>
<vn-worker-descriptor-popover
vn-id="worker-descriptor">
</vn-worker-descriptor-popover>

View File

@ -0,0 +1,84 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
constructor($element, $, vnReport) {
super($element, $);
this.vnReport = vnReport;
const now = new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
this.params = {
from: firstDayOfMonth,
to: lastDayOfMonth
};
this.$checkAll = false;
this.smartTableOptions = {
activeButtons: {
search: true,
}, columns: [
{
field: 'isActive',
searchable: false
},
{
field: 'hasToInvoice',
searchable: false
},
{
field: 'isTaxDataChecked',
searchable: false
},
]
};
}
exprBuilder(param, value) {
switch (param) {
case 'company':
return {'company': value};
case 'country':
return {'country': value};
case 'clientId':
return {'clientId': value};
case 'clientSocialName':
return {'clientSocialName': value};
case 'amount':
return {'amount': value};
case 'taxableBase':
return {'taxableBase': value};
case 'ticketFk':
return {'ticketFk': value};
case 'comercialName':
return {'comercialName': value};
}
}
downloadCSV() {
const data = [];
this.$.model._orgData.forEach(element => {
data.push(Object.keys(element).map(key => {
return {newName: this.$t(key), value: element[key]};
}).filter(item => item !== null)
.reduce((result, item) => {
result[item.newName] = item.value;
return result;
}, {}));
});
this.vnReport.show('InvoiceIns/negativeBasesCsv', {
negativeBases: data,
from: this.params.from,
to: this.params.to
});
}
}
Controller.$inject = ['$element', '$scope', 'vnReport'];
ngModule.vnComponent('vnNegativeBases', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,14 @@
Has To Invoice: Facturar
Download as CSV: Descargar como CSV
company: Compañía
country: País
clientId: Id Cliente
clientSocialName: Cliente
amount: Importe
taxableBase: Base
ticketFk: Id Ticket
isActive: Activo
hasToInvoice: Facturar
isTaxDataChecked: Datos comprobados
comercialId: Id Comercial
comercialName: Comercial

View File

@ -0,0 +1,10 @@
@import "./variables";
vn-negative-bases {
vn-date-picker{
padding-right: 5%;
}
slot-actions{
align-items: center;
}
}

View File

@ -9,14 +9,9 @@
],
"menus": {
"main": [
{
"state": "invoiceIn.index",
"icon": "icon-invoice-in"
},
{
"state": "invoiceIn.serial",
"icon": "icon-invoice-in"
}
{ "state": "invoiceIn.index", "icon": "icon-invoice-in"},
{ "state": "invoiceIn.serial", "icon": "icon-invoice-in"},
{ "state": "invoiceIn.negative-bases", "icon": "icon-ticket"}
],
"card": [
{
@ -58,6 +53,15 @@
"administrative"
]
},
{
"url": "/negative-bases",
"state": "invoiceIn.negative-bases",
"component": "vn-negative-bases",
"description": "Negative bases",
"acl": [
"administrative"
]
},
{
"url": "/serial",
"state": "invoiceIn.serial",