diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c1fc2fe..c5ee05fe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - diff --git a/db/changes/231401/.gitkeep b/db/changes/231401/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/db/changes/231401/00-negativeBases.sql b/db/changes/231401/00-negativeBases.sql new file mode 100644 index 000000000..0bdc6f2dc --- /dev/null +++ b/db/changes/231401/00-negativeBases.sql @@ -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'); diff --git a/e2e/paths/09-invoice-in/05_negative_bases.spec.js b/e2e/paths/09-invoice-in/05_negative_bases.spec.js new file mode 100644 index 000000000..4c9fe651f --- /dev/null +++ b/e2e/paths/09-invoice-in/05_negative_bases.spec.js @@ -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(); + }); +}); diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 95bf16d66..d6588c0b2 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -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" } diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml index de4b91e0b..adbca8dbf 100644 --- a/modules/client/front/locale/es.yml +++ b/modules/client/front/locale/es.yml @@ -63,4 +63,4 @@ Consumption: Consumo Compensation Account: Cuenta para compensar Amount to return: Cantidad a devolver Delivered amount: Cantidad entregada -Unpaid: Impagado \ No newline at end of file +Unpaid: Impagado diff --git a/modules/invoiceIn/back/methods/invoice-in/negativeBases.js b/modules/invoiceIn/back/methods/invoice-in/negativeBases.js new file mode 100644 index 000000000..4d5975fab --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/negativeBases.js @@ -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]; + }; +}; + diff --git a/modules/invoiceIn/back/methods/invoice-in/negativeBasesCsv.js b/modules/invoiceIn/back/methods/invoice-in/negativeBasesCsv.js new file mode 100644 index 000000000..963151b7d --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/negativeBasesCsv.js @@ -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"` + ]; + }; +}; diff --git a/modules/invoiceIn/back/methods/invoice-in/specs/negativeBases.spec.js b/modules/invoiceIn/back/methods/invoice-in/specs/negativeBases.spec.js new file mode 100644 index 000000000..a5c6e3102 --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/specs/negativeBases.spec.js @@ -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`); + }); +}); diff --git a/modules/invoiceIn/back/models/invoice-in.js b/modules/invoiceIn/back/models/invoice-in.js index 51905ccb8..167f2ac34 100644 --- a/modules/invoiceIn/back/models/invoice-in.js +++ b/modules/invoiceIn/back/models/invoice-in.js @@ -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); }; diff --git a/modules/invoiceIn/front/index.js b/modules/invoiceIn/front/index.js index e257cfee3..c0374e996 100644 --- a/modules/invoiceIn/front/index.js +++ b/modules/invoiceIn/front/index.js @@ -15,3 +15,4 @@ import './create'; import './log'; import './serial'; import './serial-search-panel'; +import './negative-bases'; diff --git a/modules/invoiceIn/front/locale/es.yml b/modules/invoiceIn/front/locale/es.yml index a2d658519..017e89dd4 100644 --- a/modules/invoiceIn/front/locale/es.yml +++ b/modules/invoiceIn/front/locale/es.yml @@ -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 diff --git a/modules/invoiceIn/front/negative-bases/index.html b/modules/invoiceIn/front/negative-bases/index.html new file mode 100644 index 000000000..368f44461 --- /dev/null +++ b/modules/invoiceIn/front/negative-bases/index.html @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Company + + Country + + Id Client + + Client + + Amount + + Base + + Id Ticket + + Active + + Has To Invoice + + Verified data + + Comercial +
{{client.company | dashIfEmpty}}{{client.country | dashIfEmpty}} + + {{::client.clientId | dashIfEmpty}} + + {{client.clientSocialName | dashIfEmpty}}{{client.amount | currency: 'EUR':2 | dashIfEmpty}}{{client.taxableBase | dashIfEmpty}} + + {{::client.ticketFk | dashIfEmpty}} + + + + + + + + + + + + + {{::client.comercialName | dashIfEmpty}} + +
+
+
+
+ + + + + + diff --git a/modules/invoiceIn/front/negative-bases/index.js b/modules/invoiceIn/front/negative-bases/index.js new file mode 100644 index 000000000..0f6f04692 --- /dev/null +++ b/modules/invoiceIn/front/negative-bases/index.js @@ -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 +}); diff --git a/modules/invoiceIn/front/negative-bases/locale/es.yml b/modules/invoiceIn/front/negative-bases/locale/es.yml new file mode 100644 index 000000000..9095eee22 --- /dev/null +++ b/modules/invoiceIn/front/negative-bases/locale/es.yml @@ -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 diff --git a/modules/invoiceIn/front/negative-bases/style.scss b/modules/invoiceIn/front/negative-bases/style.scss new file mode 100644 index 000000000..2d628cb94 --- /dev/null +++ b/modules/invoiceIn/front/negative-bases/style.scss @@ -0,0 +1,10 @@ +@import "./variables"; + +vn-negative-bases { + vn-date-picker{ + padding-right: 5%; + } + slot-actions{ + align-items: center; + } +} diff --git a/modules/invoiceIn/front/routes.json b/modules/invoiceIn/front/routes.json index 90c4f8472..40d061d1b 100644 --- a/modules/invoiceIn/front/routes.json +++ b/modules/invoiceIn/front/routes.json @@ -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",