Merge branch 'dev' into 4927-permitir-razonSocial-duplicada
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Alexandre Riera 2023-01-27 10:09:35 +00:00
commit 36a0b0b8f9
15 changed files with 347 additions and 52 deletions

View File

@ -8,13 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2304.01] - 2023-02-09
### Added
- (Rutas) Al descargar varias facturas se comprime en un zip
- (Trabajadores -> Nuevo trabajador) Nueva sección
### Changed
-
### Fixed
-
- (Artículos -> Etiquetas) Permite intercambiar la relevancia entre dos etiquetas.
## [2302.01] - 2023-01-26

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Tag', 'onSubmit', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -147,28 +147,17 @@ export default class CrudModel extends ModelProxy {
this.moreRows = null;
}
/**
* Saves current changes on the server.
*
* @return {Promise} The save request promise
*/
save() {
if (!this.isChanged)
return this.$q.resolve();
getChanges() {
if (!this.isChanged) return null;
let deletes = [];
let updates = [];
let creates = [];
let orgDeletes = [];
let orgUpdates = [];
let orgCreates = [];
const deletes = [];
const updates = [];
const creates = [];
let pk = this.primaryKey;
const pk = this.primaryKey;
for (let row of this.removed) {
for (let row of this.removed)
deletes.push(row.$orgRow[pk]);
orgDeletes.push(row);
}
for (let row of this.data) {
if (row.$isNew) {
@ -178,7 +167,6 @@ export default class CrudModel extends ModelProxy {
data[prop] = row[prop];
}
creates.push(row);
orgCreates.push(row);
} else if (row.$oldData) {
let data = {};
for (let prop in row.$oldData)
@ -187,28 +175,38 @@ export default class CrudModel extends ModelProxy {
data,
where: {[pk]: row.$orgRow[pk]}
});
orgUpdates.push(row);
}
}
let changes = {deletes, updates, creates};
const changes = {deletes, updates, creates};
for (let prop in changes) {
if (changes[prop].length === 0)
changes[prop] = undefined;
}
if (!changes)
return this.$q.resolve();
return changes;
}
/**
* Saves current changes on the server.
*
* @return {Promise} The save request promise
*/
save() {
const pk = this.primaryKey;
const changes = this.getChanges();
if (!changes) return this.$q.resolve();
const creates = changes.creates || [];
let url = this.saveUrl ? this.saveUrl : `${this._url}/crud`;
return this.$http.post(url, changes)
.then(res => {
const created = res.data;
// Apply new data to created instances
for (let i = 0; i < orgCreates.length; i++) {
const row = orgCreates[i];
for (let i = 0; i < creates.length; i++) {
const row = creates[i];
row[pk] = created[i][pk];
for (let prop in row) {

View File

@ -19,10 +19,26 @@ class Controller extends ModuleCard {
}, {
relation: 'ticket',
scope: {
fields: ['zoneFk'],
include: {
relation: 'zone'
}
fields: ['zoneFk', 'addressFk'],
include: [
{
relation: 'zone',
scope: {
fields: ['name']
}
},
{
relation: 'address',
scope: {
fields: ['provinceFk'],
include: {
relation: 'province',
scope: {
fields: ['name']
}
}
}
}]
}
}, {
relation: 'claimState',

View File

@ -55,9 +55,13 @@
<span
ng-click="zoneDescriptor.show($event, $ctrl.claim.ticket.zoneFk)"
class="link">
{{$ctrl.claim.ticket.zoneFk}}
{{$ctrl.claim.ticket.zone.name}}
</span>
</vn-label-value>
<vn-label-value
label="Province"
value="{{$ctrl.claim.ticket.address.province.name}}">
</vn-label-value>
<vn-label-value
label="Ticket">
<span

View File

@ -0,0 +1,93 @@
module.exports = function(Self) {
Self.remoteMethodCtx('onSubmit', {
description: 'Save model changes',
accessType: 'WRITE',
accepts: [
{
arg: 'creates',
type: ['object'],
description: 'The itemTags records to create'
}, {
arg: 'deletes',
type: ['number'],
description: 'The itemTags ids to delete'
}, {
arg: 'updates',
type: ['object'],
description: 'The itemTags records to update'
}, {
arg: 'maxPriority',
type: 'number',
description: 'The maxPriority value'
}
],
returns: {
root: true,
type: 'object'
},
http: {
verb: 'PATCH',
path: '/onSubmit'
}
});
Self.onSubmit = async(ctx, options) => {
const models = Self.app.models;
const args = ctx.args;
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const promises = [];
if (args.deletes) {
for (const itemTagId of args.deletes) {
const itemTagDeleted = models.ItemTag.destroyById(itemTagId, myOptions);
promises.push(itemTagDeleted);
}
}
if (args.updates) {
for (const row of args.updates) {
if (row.data.priority) {
const itemTag = await models.ItemTag.findById(row.where.id, null, myOptions);
const itemTagUpdatedPriority = itemTag.updateAttributes({
priority: row.data.priority + args.maxPriority
}, myOptions);
promises.push(itemTagUpdatedPriority);
}
}
for (const row of args.updates) {
const itemTag = await models.ItemTag.findById(row.where.id, null, myOptions);
const itemTagUpdated = itemTag.updateAttributes(row.data, myOptions);
promises.push(itemTagUpdated);
}
}
if (args.creates) {
for (const itemTag of args.creates) {
const newItemTag = models.ItemTag.create(itemTag, myOptions);
promises.push(newItemTag);
}
}
const resolvedPromises = await Promise.all(promises);
if (tx) await tx.commit();
return resolvedPromises;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,100 @@
const models = require('vn-loopback/server/server').models;
describe('tag onSubmit()', () => {
it('should delete a tag', async() => {
const tx = await models.Item.beginTransaction({});
const options = {transaction: tx};
try {
const deletes = [40];
const ctx = {
args: {
deletes: deletes
}
};
const result = await models.Tag.onSubmit(ctx, options);
expect(result.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should update a tag', async() => {
const tx = await models.Item.beginTransaction({});
const options = {transaction: tx};
try {
const updates = [{data: {value: 'Container Test'}, where: {id: 36}}];
const ctx = {
args: {
updates: updates
}
};
const result = await models.Tag.onSubmit(ctx, options);
expect(result.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should create a tag', async() => {
const tx = await models.Item.beginTransaction({});
const options = {transaction: tx};
try {
const creates = [{
'itemFk': '6',
'priority': 8,
'$orgIndex': null,
'$oldData': null,
'$isNew': true,
'tagFk': 3,
'value': 'madera'
}];
const ctx = {
args: {
creates: creates
}
};
const result = await models.Tag.onSubmit(ctx, options);
expect(result.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should swap priority for two tags', async() => {
const tx = await models.Item.beginTransaction({});
const options = {transaction: tx};
try {
const updates = [
{data: {priority: 2}, where: {id: 36}},
{data: {priority: 1}, where: {id: 37}}
];
const ctx = {
args: {
updates: updates,
maxPriority: 7,
}
};
const result = await models.Tag.onSubmit(ctx, options);
expect(result.length).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -1,3 +1,4 @@
module.exports = Self => {
require('../methods/tag/filterValue')(Self);
require('../methods/tag/onSubmit')(Self);
};

View File

@ -19,7 +19,7 @@
data="tags"
auto-load="true">
</vn-crud-model>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<form name="form" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal ng-repeat="itemTag in $ctrl.itemTags">
<vn-autocomplete vn-two vn-id="tag" vn-focus
@ -74,8 +74,9 @@
</vn-card>
<vn-button-bar>
<vn-submit
ng-click="$ctrl.onSubmit()"
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
</vn-button-bar>
</form>
</form>

View File

@ -29,11 +29,17 @@ class Controller extends Section {
}
onSubmit() {
this.$.watcher.check();
this.$.model.save().then(() => {
const changes = this.$.model.getChanges();
const data = {
creates: changes.creates,
deletes: changes.deletes,
updates: changes.updates,
maxPriority: this.getHighestPriority()
};
this.$http.patch(`Tags/onSubmit`, data).then(() => {
this.$.model.refresh();
this.$.watcher.notifySaved();
this.$.watcher.updateOriginalData();
this.card.reload();
});
}
}

View File

@ -0,0 +1,62 @@
const JSZip = require('jszip');
module.exports = Self => {
Self.remoteMethodCtx('downloadZip', {
description: 'Download a zip file with multiple routes pdfs',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'string',
description: 'The routes ids',
}
],
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: '/downloadZip',
verb: 'GET'
}
});
Self.downloadZip = async function(ctx, id, options) {
const models = Self.app.models;
const myOptions = {};
const zip = new JSZip();
if (typeof options == 'object')
Object.assign(myOptions, options);
const ids = id.split(',');
for (let id of ids) {
ctx.args.id = id;
const routePdf = await models.Route.driverRoutePdf(ctx, id);
const fileName = extractFileName(routePdf[2]);
const body = routePdf[0];
zip.file(fileName, body);
}
const stream = zip.generateNodeStream({streamFiles: true});
return [stream, 'application/zip', `filename="download.zip"`];
};
function extractFileName(str) {
const matches = str.match(/"(.*?)"/);
return matches ? matches[1] : str;
}
};

View File

@ -13,6 +13,7 @@ module.exports = Self => {
require('../methods/route/driverRoutePdf')(Self);
require('../methods/route/driverRouteEmail')(Self);
require('../methods/route/sendSms')(Self);
require('../methods/route/downloadZip')(Self);
Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000'

View File

@ -34,12 +34,22 @@ export default class Controller extends Section {
}
showRouteReport() {
const routes = [];
const routesIds = [];
for (let route of this.checked)
routes.push(route.id);
const routesId = routes.join(',');
routesIds.push(route.id);
const stringRoutesIds = routesIds.join(',');
this.vnReport.show(`Routes/${routesId}/driver-route-pdf`);
if (this.checked.length <= 1) {
const url = `api/Routes/${stringRoutesIds}/driver-route-pdf?access_token=${this.vnToken.token}`;
window.open(url, '_blank');
} else {
const serializedParams = this.$httpParamSerializer({
access_token: this.vnToken.token,
id: stringRoutesIds
});
const url = `api/Routes/downloadZip?${serializedParams}`;
window.open(url, '_blank');
}
}
openClonationDialog() {

View File

@ -44,17 +44,15 @@ describe('Component vnRouteIndex', () => {
describe('showRouteReport()', () => {
it('should call to the vnReport show method', () => {
controller.vnReport.show = jest.fn();
jest.spyOn(window, 'open').mockReturnThis();
const data = controller.$.model.data;
data[0].checked = true;
data[2].checked = true;
const routeIds = '1,3';
controller.showRouteReport();
expect(controller.vnReport.show).toHaveBeenCalledWith(`Routes/${routeIds}/driver-route-pdf`);
expect(window.open).toHaveBeenCalled();
});
});

View File

@ -1,12 +1,12 @@
{
"module": "shelving",
"name": "Shelvings",
"icon" : "contact_support",
"icon" : "icon-inventory",
"dependencies": ["worker"],
"validations" : true,
"menus": {
"main": [
{"state": "shelving.index", "icon": "contact_support"}
{"state": "shelving.index", "icon": "icon-inventory"}
],
"card": [
{"state": "shelving.card.basicData", "icon": "settings"},
@ -20,7 +20,7 @@
"abstract": true,
"component": "vn-shelving",
"description": "Shelvings"
},
},
{
"url": "/index?q",
"state": "shelving.index",
@ -32,13 +32,13 @@
"state": "shelving.create",
"component": "vn-shelving-create",
"description": "New shelving"
},
},
{
"url": "/:id",
"state": "shelving.card",
"abstract": true,
"component": "vn-shelving-card"
},
},
{
"url": "/summary",
"state": "shelving.card.summary",
@ -47,7 +47,7 @@
"params": {
"shelving": "$ctrl.shelving"
}
},
},
{
"url": "/basic-data",
"state": "shelving.card.basicData",
@ -56,7 +56,7 @@
"params": {
"shelving": "$ctrl.shelving"
}
},
},
{
"url" : "/log",
"state": "shelving.card.log",
@ -64,4 +64,4 @@
"description": "Log"
}
]
}
}