231801_test_to_master #1519

Merged
alexm merged 490 commits from 231801_test_to_master into master 2023-05-12 06:29:59 +00:00
15 changed files with 392 additions and 10 deletions
Showing only changes of commit f2bd0253e2 - Show all commits

View File

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

View File

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

View File

@ -150,5 +150,7 @@
"Tickets with associated refunds": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº {{id}}",
"It is not possible to modify tracked sales": "It is not possible to modify tracked sales",
"It is not possible to modify sales that their articles are from Floramondo": "It is not possible to modify sales that their articles are from Floramondo",
"It is not possible to modify cloned sales": "It is not possible to modify cloned sales"
"It is not possible to modify cloned sales": "It is not possible to modify cloned sales",
"Valid priorities: 1,2,3": "Valid priorities: 1,2,3",
"Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2"
}

View File

@ -266,5 +266,6 @@
"There is no assigned email for this client": "No hay correo asignado para este cliente",
"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"
"Not exist this branch": "La rama no existe",
"Insert a date range": "Inserte un rango de fechas"
}

View File

@ -64,3 +64,4 @@ Compensation Account: Cuenta para compensar
Amount to return: Cantidad a devolver
Delivered amount: Cantidad entregada
Unpaid: Impagado
Unbilled tickets: Tickets sin facturar

View File

@ -0,0 +1,47 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn unbilledTickets()', () => {
it('should return all unbilled tickets 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.unbilledTickets(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.unbilledTickets(ctx, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`Insert a date range`);
});
});

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('unbilledTickets', {
description: 'Find all unbilled tickets',
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: `/unbilledTickets`,
verb: 'GET'
}
});
Self.unbilledTickets = 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
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 ticketsIndex = 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 ticketsIndex === 0 ? result : result[ticketsIndex];
};
};

View File

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

View File

@ -13,3 +13,4 @@ import './dueDay';
import './intrastat';
import './create';
import './log';
import './unbilled-tickets';

View File

@ -9,10 +9,8 @@
],
"menus": {
"main": [
{
"state": "invoiceIn.index",
"icon": "icon-invoice-in"
}
{ "state": "invoiceIn.index", "icon": "icon-invoice-in"},
{ "state": "invoiceIn.unbilled-tickets", "icon": "icon-ticket"}
],
"card": [
{
@ -54,6 +52,15 @@
"administrative"
]
},
{
"url": "/unbilled-tickets",
"state": "invoiceIn.unbilled-tickets",
"component": "vn-unbilled-tickets",
"description": "Unbilled tickets",
"acl": [
"administrative"
]
},
{
"url": "/:id",
"state": "invoiceIn.card",

View File

@ -0,0 +1,104 @@
<vn-crud-model
vn-id="model"
url="InvoiceIns/unbilledTickets"
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>
</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" center>
<span translate>Active</span>
</th>
<th field="hasToInvoice" center>
<span translate>Has To Invoice</span>
</th>
<th field="isTaxDataChecked" center>
<span translate>Verified data</span>
</th>
<th field="comercialId">
<span translate>Id Comercial</span>
</th>
<th field="comercialName">
<span translate>Comercial</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="ticket in model.data">
<td>{{ticket.company | dashIfEmpty}}</td>
<td>{{ticket.country | dashIfEmpty}}</td>
<td>{{ticket.clientId | dashIfEmpty}}</td>
<td>{{ticket.clientSocialName | dashIfEmpty}}</td>
<td>{{ticket.amount | currency: 'EUR':2 | dashIfEmpty}}</td>
<td>{{ticket.taxableBase | dashIfEmpty}}</td>
<td>{{ticket.ticketFk | dashIfEmpty}}</td>
<td center>
<vn-check
disabled="true"
ng-model="ticket.isActive">
</vn-check>
</td>
<td center>
<vn-check
disabled="true"
ng-model="ticket.hasToInvoice">
</vn-check>
</td>
<td center>
<vn-check
disabled="true"
ng-model="ticket.isTaxDataChecked">
</vn-check>
</td>
<td>{{ticket.comercialId | dashIfEmpty}}</td>
<td>{{ticket.comercialName | dashIfEmpty}}</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>

View File

@ -0,0 +1,66 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
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 'comercialId':
return {'comercialId': value};
case 'comercialName':
return {'comercialName': value};
}
}
}
Controller.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnUnbilledTickets', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1 @@
Has To Invoice: Facturar

View File

@ -0,0 +1,7 @@
@import "./variables";
vn-unbilled-tickets {
vn-date-picker{
padding-right: 5%;
}
}