Merge branch 'dev' of https: refs #7031//gitea.verdnatura.es/verdnatura/salix into 7031-zonePrice

This commit is contained in:
Carlos Satorres 2024-12-10 12:44:52 +01:00
commit 535b04e1df
32 changed files with 380 additions and 295 deletions

44
Jenkinsfile vendored
View File

@ -7,7 +7,8 @@ def RUN_BUILD
def BRANCH_ENV = [
test: 'test',
master: 'production'
master: 'production',
beta: 'production'
]
node {
@ -18,7 +19,8 @@ node {
PROTECTED_BRANCH = [
'dev',
'test',
'master'
'master',
'beta'
].contains(env.BRANCH_NAME)
FROM_GIT = env.JOB_NAME.startsWith('gitea/')
@ -62,6 +64,18 @@ pipeline {
PROJECT_NAME = 'salix'
}
stages {
stage('Version') {
when {
expression { RUN_BUILD }
}
steps {
script {
def packageJson = readJSON file: 'package.json'
def version = "${packageJson.version}-build${env.BUILD_ID}"
writeFile(file: 'VERSION.txt', text: version)
}
}
}
stage('Install') {
environment {
NODE_ENV = ''
@ -118,11 +132,10 @@ pipeline {
when {
expression { RUN_BUILD }
}
environment {
VERSION = readFile 'VERSION.txt'
}
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
sh 'docker-compose build back'
}
}
@ -156,11 +169,10 @@ pipeline {
when {
expression { RUN_BUILD }
}
environment {
VERSION = readFile 'VERSION.txt'
}
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
sh 'gulp build'
sh 'docker-compose build front'
}
@ -175,12 +187,9 @@ pipeline {
}
environment {
CREDENTIALS = credentials('docker-registry')
VERSION = readFile 'VERSION.txt'
}
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
sh 'docker login --username $CREDENTIALS_USR --password $CREDENTIALS_PSW $REGISTRY'
sh 'docker-compose push'
}
@ -207,11 +216,10 @@ pipeline {
when {
expression { FROM_GIT }
}
environment {
VERSION = readFile 'VERSION.txt'
}
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
withKubeConfig([
serverUrl: "$KUBERNETES_API",
credentialsId: 'kubernetes',

View File

@ -1,88 +0,0 @@
const axios = require('axios');
const UserError = require('vn-loopback/util/user-error');
const moment = require('moment');
module.exports = Self => {
Self.remoteMethod('sendOrders', {
description: 'Sends a set of orders',
accessType: 'WRITE',
accepts: [{
arg: 'tickets',
type: ['number'],
required: true
}
],
returns: {
type: 'string',
root: true
},
http: {
path: `/sendOrders`,
verb: 'POST'
}
});
Self.sendOrders = async tickets => {
const config = await Self.app.models.QuadmindsApiConfig.findOne();
if (!config) throw new UserError('Config params not set');
if (tickets.length > config.maxObjects)
throw new UserError(`Quadminds does not support more than ${config.maxObjects} tickets`);
let poisData = [];
let isOk;
for (let offset = 0; !isOk; offset = offset + config.limit) {
const pois = await axios.get(`${config.url}pois/search?limit=${config.limit}&offset=${offset}`, {
headers: {
'Accept': 'application/json',
'X-Saas-Apikey': config.key
}
});
pois.data.data.length ? poisData.push(...pois.data.data) : isOk = true;
}
const poiMap = new Map(poisData.map(poi => [poi.code, poi._id]));
let orders = await Self.rawSql(`
SELECT a.id poiCode,
t.id code,
t.shipped date,
'PEDIDO' operation,
t.totalWithVat totalAmount,
t.totalWithoutVat totalAmountWithoutTaxes,
SUM(sv.volume) volume
FROM ticket t
JOIN address a ON a.id = t.addressFk
JOIN saleVolume sv ON sv.ticketFk = t.id
WHERE t.id IN (?)
GROUP BY t.id
`, [tickets]);
// Transformo code en string ya que lo obtenermos como integer
orders = orders.map(order => {
return {
...order,
poiId: poiMap.get(order.poiCode.toString()) || undefined,
code: order.code.toString(),
date: moment(order.date).format('YYYY-MM-DD'),
totalAmount: order.totalAmount || undefined,
totalAmountWithoutTaxes: order.totalAmountWithoutTaxes || undefined,
timeWindow: [{
from: config.orderTimeFrom,
to: config.orderTimeTo
}],
orderMeasures: [{
constraintId: 3, // Volumen
value: order.volume
}]
};
});
await axios.post(`${config.url}orders`, orders, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Saas-Apikey': config.key
}
});
};
};

View File

@ -1,87 +0,0 @@
const axios = require('axios');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('sendPois', {
description: 'Sends a set of pois',
accessType: 'WRITE',
accepts: [{
arg: 'tickets',
type: ['number'],
required: true
}
],
returns: {
type: 'string',
root: true
},
http: {
path: `/sendPois`,
verb: 'POST'
}
});
Self.sendPois = async tickets => {
const config = await Self.app.models.QuadmindsApiConfig.findOne();
if (!config) throw new UserError('Config params not set');
if (tickets.length > config.maxObjects)
throw new UserError(`Quadminds does not support more than ${config.maxObjects} tickets`);
let pois = await Self.rawSql(`
WITH deliveryNotes AS (
SELECT t.id, t.routeFk, tn.description
FROM ticket t
JOIN ticketObservation tn ON tn.ticketFk = t.id
JOIN observationType ot ON ot.id = tn.observationTypeFk
WHERE ot.code = 'delivery'
)
SELECT a.id code,
c.socialName name,
IF(ABS(a.latitude - ROUND(a.latitude)) < 0.000001, NULL, a.latitude) latitude,
IF(ABS(a.longitude - ROUND(a.longitude)) < 0.000001, NULL, a.longitude) longitude,
a.street,
a.city locality,
p.name state,
co.name country,
CONCAT_WS(', ', IFNULL(a.street, ''), IFNULL(a.city, ''), IFNULL(p.name, '')) longAddress,
CONCAT(IFNULL(a.mobile, c.mobile)) phoneNumber,
dn.description poiDeliveryComments,
c.email email
FROM ticket t
JOIN address a ON a.id = t.addressFk
JOIN province p ON p.id = a.provinceFk
JOIN country co ON co.id = p.countryFk
JOIN client c ON c.id = t.clientFk
LEFT JOIN deliveryNotes dn ON dn.id = t.id
WHERE t.id IN (?)
GROUP BY t.id
`, [tickets]);
// Transformo code en string ya que lo obtenermos como integer
pois = pois.map(poi => {
return {
...poi,
code: poi.code.toString(),
latitude: poi.latitude || undefined,
longitude: poi.longitude || undefined,
address: {
street: poi.street || undefined,
locality: poi.locality || undefined,
state: poi.state || undefined,
country: poi.country || undefined
},
poiDeliveryComments: poi.poiDeliveryComments || undefined,
phoneNumber: poi.phoneNumber || undefined,
email: poi.email || undefined
};
});
await axios.post(`${config.url}pois`, pois, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Saas-Apikey': config.key
}
});
};
};

View File

@ -121,9 +121,6 @@
"Province": {
"dataSource": "vn"
},
"QuadmindsApiConfig": {
"dataSource": "vn"
},
"Autonomy": {
"dataSource": "vn"
},

View File

@ -20,6 +20,9 @@
},
"backupPrinterNotificationDelay": {
"type": "string"
},
"itemOrderReviewHours": {
"type": "number"
}
}
}

View File

@ -1,4 +0,0 @@
module.exports = Self => {
require('../methods/quadminds-api-config/sendPois')(Self);
require('../methods/quadminds-api-config/sendOrders')(Self);
};

View File

@ -1,34 +0,0 @@
{
"name": "QuadmindsApiConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "quadmindsApiConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"required": true
},
"url": {
"type": "string"
},
"key": {
"type": "string"
},
"maxObjects": {
"type": "number"
},
"limit": {
"type": "number"
},
"orderTimeFrom": {
"type": "string"
},
"orderTimeTo": {
"type": "string"
}
}
}

View File

@ -3772,7 +3772,8 @@ VALUES
(999992, 18, 50, '2023-09-21', NULL, 1, NULL, 103, NULL),
(1000000, 18, 25, '2023-09-21', 25, 500, NULL, 103, NULL),
(999996, 19, 5, '2023-09-27', 1, 5, NULL, 103, NULL),
(999997, 21, 10, '2023-09-27', 5, 100, NULL, 103, NULL);
(999997, 21, 10, '2023-09-27', 5, 100, NULL, 103, NULL),
(1000000, 16, 25, '2023-08-21',25, 500, NULL, NULL, NULL);
-- Previous for Bolas de madera
INSERT IGNORE INTO vn.sectorCollection

View File

@ -1,15 +0,0 @@
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `bi`.`rotacion`
AS SELECT `ic`.`itemFk` AS `Id_Article`,
`ic`.`warehouseFk` AS `warehouse_id`,
`ic`.`quantity` AS `total`,
`ic`.`rotation` AS `rotacion`,
`ic`.`cm3` AS `cm3`,
`ic`.`storage` AS `almacenaje`,
`ic`.`handling` AS `manipulacion`,
`ic`.`extraCharge` AS `auxiliar`,
`ic`.`wasted` AS `mermas`,
`ic`.`cm3delivery` AS `cm3reparto`,
`ic`.`grams` AS `grams`
FROM `vn`.`itemCost` `ic`

View File

@ -0,0 +1,2 @@
INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId)
VALUES ('Worker','canCreateAbsenceInPast','WRITE','ALLOW','ROLE','hr');

View File

@ -0,0 +1,5 @@
-- Place your SQL code here
ALTER TABLE vn.productionConfig ADD itemOrderReviewHours int(11) DEFAULT 24 NULL
COMMENT 'Horas que no se tienen en cuenta para comprobar orden en el almacén, null para desactivar revisión';

View File

@ -0,0 +1 @@
DROP TABLE vn.quadmindsApiConfig;

View File

@ -246,7 +246,8 @@
"ticketLostExpedition": "The ticket [{{ticketId}}]({{{ticketUrl}}}) has the following lost expedition:{{ expeditionId }}",
"The raid information is not correct": "The raid information is not correct",
"Payment method is required": "Payment method is required",
"Sales already moved": "Sales already moved",
"Sales already moved": "Sales already moved",
"Holidays to past days not available": "Holidays to past days not available",
"There are tickets to be invoiced": "There are tickets to be invoiced for this zone, please delete them first",
"Price cannot be blank": "Price cannot be blank"
}
}

View File

@ -391,5 +391,7 @@
"Sales already moved": "Ya han sido transferidas",
"The raid information is not correct": "La información de la redada no es correcta",
"There are tickets to be invoiced": "Hay tickets para esta zona, borralos primero",
"Price cannot be blank": "Price cannot be blank"
}
"Price cannot be blank": "Price cannot be blank",
"An item type with the same code already exists": "Un tipo con el mismo código ya existe",
"Holidays to past days not available": "Las vacaciones a días pasados no están disponibles"
}

View File

@ -9,6 +9,10 @@ module.exports = Self => {
required: true,
description: 'The entry id',
http: {source: 'path'}
}, {
arg: 'showEntryLines',
type: 'boolean',
required: false
}
],
returns: [

View File

@ -47,8 +47,7 @@ module.exports = Self => {
for (const buy of buys) {
if (buy.stickers < 1) continue;
ctx.args.id = buy.id;
ctx.args.copies = buy.stickers;
ctx.args = {...ctx.args, id: buy.id, showEntryLines: true};
const pdfBuffer = await models.Entry.buyLabelSupplier(ctx, myOptions);
await merger.add(new Uint8Array(pdfBuffer[0]));
}

View File

@ -14,6 +14,12 @@ module.exports = Self => {
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
},
{
arg: 'id',
type: 'number',
description: 'The invoiceOut id',
http: {source: 'query'}
},
{
arg: 'search',
type: 'string',
@ -106,6 +112,8 @@ module.exports = Self => {
return {'i.created': value};
case 'clientFk':
return {'i.clientFk': value};
case 'id':
return {'i.id': value};
case 'fi':
return {'c.fi': value};
case 'amount':

View File

@ -0,0 +1,5 @@
name: Minimum Quantity
columns:
ended: Ended
code: Code
started: Started

View File

@ -0,0 +1,5 @@
name: Cantidad Mínima
columns:
ended: Finaliza
quantity: Cantidad
started: Comienza

View File

@ -46,4 +46,4 @@ columns:
itemFk: item
density: density
compression: compression
minQuantity: min quantity

View File

@ -46,4 +46,4 @@ columns:
itemFk: artículo
density: densidad
compression: compresión
minQuantity: Cantidad mínima

View File

@ -0,0 +1,89 @@
module.exports = Self => {
const models = require('vn-loopback/server/server').models;
Self.remoteMethod('getItemsByReviewOrder', {
description:
'Get list items if they are ordered by pickingOrder and their created regarding where they will be parked',
accessType: 'READ',
accepts: [{
arg: 'shelving',
type: 'string',
required: true,
description: 'Shelving code'
},
{
arg: 'parking',
type: 'string',
required: true,
description: 'Parking code'
},
{
arg: 'itemFk',
type: 'number',
description: 'Item id'
},
],
returns: {
type: 'Array',
root: true
},
http: {
path: `/getItemsByReviewOrder`,
verb: 'GET'
}
});
Self.getItemsByReviewOrder = async(shelving, parking, itemFk, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const config = await models.ProductionConfig.findOne();
const hoursToCompare = config['itemOrderReviewHours'];
if (!hoursToCompare) return [];
const parkingItem = await models.Parking.findOne({where: {code: parking}}, myOptions);
if (!parkingItem) return [];
const pickingOrderToCompare = parkingItem['pickingOrder'];
const result = await Self.rawSql(`
WITH currentItemShelving AS (
SELECT is2.created, is2.itemFk, sh.code
FROM vn.itemShelving is2
JOIN vn.shelving sh ON sh.id = is2.shelvingFk
LEFT JOIN vn.parking p ON p.id = sh.parkingFk
LEFT JOIN vn.sector s ON s.id = p.sectorFk
WHERE sh.code = ? AND (? IS NULL OR is2.itemFk = ?)
),
itemShelvings AS (
SELECT is2.itemFk, is2.created, sh.code, p.pickingOrder, p.code AS parkingFk
FROM vn.itemShelving is2
JOIN currentItemShelving ai ON is2.itemFk = ai.itemFk
JOIN vn.shelving sh ON sh.id = is2.shelvingFk AND ai.code <> sh.code
JOIN vn.parking p ON p.id = sh.parkingFk
JOIN vn.sector s ON s.id = p.sectorFk
),
parkingDestiny AS (
SELECT ? AS pickingOrder
)
SELECT ish.*,
CASE
WHEN ish.pickingOrder < d.pickingOrder AND aish.created < ish.created
AND ABS(TIMESTAMPDIFF(HOUR, aish.created, ish.created)) > ? THEN "old"
WHEN ish.pickingOrder > d.pickingOrder AND aish.created > ish.created
AND ABS(TIMESTAMPDIFF(HOUR, aish.created, ish.created)) > ? THEN "new"
END AS itemCreated
FROM itemShelvings ish
JOIN parkingDestiny d ON d.pickingOrder IS NOT NULL
JOIN currentItemShelving aish ON ish.itemFk = aish.itemFk
WHERE ABS(TIMESTAMPDIFF(HOUR, aish.created, ish.created)) > ?
AND (
(ish.pickingOrder < d.pickingOrder AND aish.created < ish.created)
OR
(ish.pickingOrder > d.pickingOrder AND aish.created > ish.created)
);
`,
[shelving, itemFk, itemFk, pickingOrderToCompare,
hoursToCompare, hoursToCompare, hoursToCompare, hoursToCompare], myOptions);
return result;
};
};

View File

@ -0,0 +1,123 @@
const {models} = require('vn-loopback/server/server');
describe('itemShelving getItemsByReviewOrder()', () => {
it('should return empty because hoursToReview = 0', async() => {
const shelving = 'NBB';
const parking = '700-01';
const tx = await models.Sector.beginTransaction({});
const myOptions = {transaction: tx};
try {
const config = await models.ProductionConfig.findOne();
await config.updateAttributes({
itemOrderReviewHours: null,
});
const result = await models.ItemShelving.getItemsByReviewOrder(shelving, parking, myOptions);
expect(result.length).toEqual(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return an item because you are trying parking a shelving and there is an older item', async() => {
const shelving = 'NBB';
const parking = 'K-26-2';
const itemFk = 1000000;
const tx = await models.Sector.beginTransaction({});
const myOptions = {transaction: tx};
try {
const config = await models.ProductionConfig.findOne();
await config.updateAttributes({
itemOrderReviewHours: 24,
});
const result = await models.ItemShelving.getItemsByReviewOrder(shelving, parking, itemFk, myOptions);
expect(result.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return an item because you are trying parking a shelving and there is an newer item', async() => {
const shelving = 'NBB';
const parking = 'K-26-2';
const itemFk = 1000000;
const tx = await models.Sector.beginTransaction({});
const myOptions = {transaction: tx};
try {
const config = await models.ProductionConfig.findOne();
await config.updateAttributes({
itemOrderReviewHours: 24,
});
const result = await models.ItemShelving.getItemsByReviewOrder(shelving, parking, itemFk, myOptions);
expect(result.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return a item list because you are trying parking a shelving and there is an newer item', async() => {
const shelving = 'NCC';
const parking = 'K-26-2';
const itemFk = 1000000;
const tx = await models.Sector.beginTransaction({});
const myOptions = {transaction: tx};
try {
const config = await models.ProductionConfig.findOne();
await config.updateAttributes({
itemOrderReviewHours: 24,
});
const result = await models.ItemShelving.getItemsByReviewOrder(shelving, parking, itemFk, myOptions);
expect(result.length).toEqual(2);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return empty list because all order is correct', async() => {
const shelving = 'NCC';
const parking = 'A-01-1';
const itemFk = 1000000;
const tx = await models.Sector.beginTransaction({});
const myOptions = {transaction: tx};
try {
const config = await models.ProductionConfig.findOne();
await config.updateAttributes({
itemOrderReviewHours: 24,
});
const result = await models.ItemShelving.getItemsByReviewOrder(shelving, parking, itemFk, myOptions);
expect(result.length).toEqual(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -5,4 +5,5 @@ module.exports = Self => {
require('../methods/item-shelving/getAlternative')(Self);
require('../methods/item-shelving/updateFromSale')(Self);
require('../methods/item-shelving/getListItemNewer')(Self);
require('../methods/item-shelving/getItemsByReviewOrder')(Self);
};

View File

@ -0,0 +1,9 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
return new UserError(`An item type with the same code already exists`);
return err;
});
};

View File

@ -88,6 +88,11 @@ module.exports = Self => {
arg: 'alertLevel',
type: 'number',
description: `The alert level of the tickets`
},
{
arg: 'countryFk',
type: 'number',
description: 'The country id filter'
}
],
returns: {
@ -182,6 +187,7 @@ module.exports = Self => {
t.totalWithVat,
io.id invoiceOutId,
a.provinceFk,
p.countryFk,
p.name province,
w.name warehouse,
am.name agencyMode,
@ -356,6 +362,7 @@ module.exports = Self => {
}
case 'agencyModeFk':
case 'warehouseFk':
case 'countryFk':
param = `f.${param}`;
return {[param]: value};
}

View File

@ -1,4 +1,3 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
@ -87,7 +86,7 @@ module.exports = Self => {
return /^\d+$/.test(value)
? {'t.id': value}
: {'t.ref': {like: `%${value}%`}};
case 'ref':
case 'reference':
return {'t.ref': {like: `%${value}%`}};
case 'shippedFrom':
return {'t.shipped': {gte: value}};
@ -115,42 +114,39 @@ module.exports = Self => {
`CREATE TEMPORARY TABLE tmp.travel
(INDEX (id))
ENGINE = MEMORY
SELECT
t.id,
SELECT t.id,
t.ref,
t.shipped,
t.landed,
t.kg,
am.id AS agencyModeFk,
am.name AS agencyModeName,
wo.id AS warehouseOutFk,
wo.name AS warehouseOutName,
w.name AS warehouseInFk,
w.name AS warehouseInName,
SUM(b.stickers) AS stickers,
s.id AS cargoSupplierFk,
s.nickname AS cargoSupplierNickname,
s.name AS supplierName,
CAST(SUM(b.weight * b.stickers) as DECIMAL(10,0)) as loadedKg,
am.id agencyModeFk,
am.name agencyModeName,
wo.id warehouseOutFk,
wo.name warehouseOutName,
w.name warehouseInFk,
w.name warehouseInName,
SUM(b.stickers) stickers,
s.id cargoSupplierFk,
s.nickname cargoSupplierNickname,
s.name supplierName,
CAST(SUM(b.weight * b.stickers) AS DECIMAL(10,0)) loadedKg,
CAST(
SUM(
vc.aerealVolumetricDensity *
b.stickers *
IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000
) as DECIMAL(10,0)
) as volumeKg,
GREATEST(
CAST(SUM(b.weight * b.stickers) AS INT),
CAST(
) AS DECIMAL(10,0)
) volumeKg,
CAST(
GREATEST(
SUM(b.weight * b.stickers) ,
SUM(vc.aerealVolumetricDensity *
b.stickers *
IF(pkg.volume,
pkg.volume,
pkg.width * pkg.depth * pkg.height
) / 1000000
) AS INT
)
/ t.kg * 100, 0) percentageKg
IF(pkg.volume,
pkg.volume,
pkg.width * pkg.depth * pkg.height) / 1000000)
) / t.kg * 100 AS INT
) percentageKg
FROM travel t
LEFT JOIN supplier s ON s.id = t.cargoSupplierFk
LEFT JOIN entry e ON e.travelFk = t.id

View File

@ -58,17 +58,24 @@ module.exports = Self => {
if (!isSubordinate || (isSubordinate && userId == id && !isTeamBoss))
throw new UserError(`You don't have enough privileges`);
const canCreateAbsenceInPast =
await models.ACL.checkAccessAcl(ctx, 'Worker', 'canCreateAbsenceInPast', 'WRITE');
const now = Date.vnNew();
const newDate = new Date(args.dated).getTime();
if ((now.getTime() > newDate) && !canCreateAbsenceInPast)
throw new UserError(`Holidays to past days not available`);
const labour = await models.WorkerLabour.findById(args.businessFk,
{fields: ['started', 'ended', 'businessFk']}, myOptions);
if (args.dated < labour.started || (labour.ended != null && args.dated > labour.ended))
throw new UserError(`The contract was not active during the selected date`);
query = `SELECT *
FROM vn.workerTimeControl
WHERE userFk = ? AND timed BETWEEN DATE(?) AND CONCAT(DATE(?), ' 23:59:59')
LIMIT 1;`;
const [hasHoursRecorded] = await Self.rawSql(query, [id, args.dated, args.dated]);
const [hasHoursRecorded] = await Self.rawSql(`SELECT *
FROM vn.workerTimeControl
WHERE userFk = ? AND timed BETWEEN DATE(?) AND CONCAT(DATE(?), ' 23:59:59')
LIMIT 1;`, [id, args.dated, args.dated]);
const absenceType = await models.AbsenceType.findById(args.absenceTypeId, null, myOptions);
@ -80,7 +87,6 @@ module.exports = Self => {
throw new UserError(`The worker has hours recorded that day`);
const date = Date.vnNew();
const now = Date.vnNew();
date.setHours(0, 0, 0, 0);
const [result] = await Self.rawSql(
`SELECT COUNT(*) halfHolidayCounter

View File

@ -162,4 +162,33 @@ describe('Worker createAbsence()', () => {
expect(error.message).toEqual(`The worker has hours recorded that day`);
});
it(`Should throw an error when adding a "Vacation" absence on a past day`, async() => {
const ctx = {
req: {accessToken: {userId: 19}},
args: {
id: 1110,
businessFk: 1110,
absenceTypeId: 1,
dated: '2000-12-27T23:00:00.000Z',
}
};
const workerId = 19;
const tx = await app.models.Calendar.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await app.models.Worker.createAbsence(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual(`Holidays to past days not available`);
});
});

View File

@ -86,7 +86,7 @@
<td>
<div class="cell">
<span class="lbl">{{$t('boxNum')}}</span>
{{`${buy.labelNum} / ${buy.maxLabelNum}`}}
{{getTotal(buy)}}
</div>
</td>
</tr>

View File

@ -9,7 +9,7 @@ module.exports = {
mixins: [vnReport],
async serverPrefetch() {
const buy = await models.Buy.findById(this.id, null);
this.buys = await this.rawSqlFromDef('buy', [buy.entryFk, buy.entryFk, buy.entryFk, this.id]);
this.buys = await this.rawSqlFromDef('buy', [buy.entryFk, buy.entryFk, buy.entryFk, this.id, this.id]);
const date = new Date();
this.weekNum = moment(date).isoWeek();
this.dayNum = moment(date).day();
@ -27,6 +27,11 @@ module.exports = {
height: 115,
});
return new XMLSerializer().serializeToString(svgNode);
},
getTotal(buy) {
return (this.showEntryLines) ?
`${buy.entryLabelNum} / ${buy.entryLabels}` :
`${buy.buyLabelNum} / ${buy.buyLabels}`;
}
},
props: {
@ -34,6 +39,10 @@ module.exports = {
type: Number,
required: true,
description: 'The entry id'
},
showEntryLines: {
type: Boolean,
required: false
}
}
};

View File

@ -10,7 +10,7 @@ WITH RECURSIVE numbers AS (
)
),
labels AS (
SELECT ROW_NUMBER() OVER(ORDER BY b.id, num.n) labelNum,
SELECT ROW_NUMBER() OVER(ORDER BY b.id, num.n) entryLabelNum,
i.name,
i.`size`,
i.category,
@ -33,6 +33,9 @@ labels AS (
WHERE b.entryFk = ?
AND num.n <= b.stickers
)
SELECT *, (SELECT SUM(stickers) FROM buy WHERE entryFk = ?) maxLabelNum
SELECT *,
ROW_NUMBER() OVER(ORDER BY entryLabelNum) buyLabelNum,
(SELECT SUM(stickers) FROM buy WHERE entryFk = ?) entryLabels,
(SELECT stickers FROM buy WHERE id = ?) buyLabels
FROM labels
WHERE id = ?