Merge pull request '#6321 - Negative tickets' (!1945) from 6321_negative_tickets into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #1945
Reviewed-by: Javi Gallego <jgallego@verdnatura.es>
This commit is contained in:
Javier Segarra 2025-02-11 08:45:32 +00:00
commit 5df24d0e70
27 changed files with 1027 additions and 180 deletions

View File

@ -798,7 +798,8 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF
(34, 1, 1, 1, 3, util.VN_CURDATE(), util.VN_CURDATE(), 1103, 'BEJAR', 123, NULL, 0, 1, 16, 0, util.VN_CURDATE(), NULL, NULL, NULL, NULL),
(35, 1, 1, 1, 3, util.VN_CURDATE(), util.VN_CURDATE(), 1102, 'Somewhere in Philippines', 123, NULL, 0, 1, 16, 0, util.VN_CURDATE(), NULL, NULL, NULL, NULL),
(36, 1, 1, 1, 3, util.VN_CURDATE(), util.VN_CURDATE(), 1102, 'Ant-Man Adventure', 123, NULL, 0, 1, 16, 0, util.VN_CURDATE(), NULL, NULL, NULL, NULL),
(37, 1, 1, 1, 3, util.VN_CURDATE(), util.VN_CURDATE(), 1110, 'Deadpool swords', 123, NULL, 0, 1, 16, 0, util.VN_CURDATE(), NULL, NULL, NULL, NULL);
(37, 1, 1, 1, 3, util.VN_CURDATE(), util.VN_CURDATE(), 1110, 'Deadpool swords', 123, NULL, 0, 1, 16, 0, util.VN_CURDATE(), NULL, NULL, NULL, NULL),
(1000000, NULL, 1, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1, 'employee', 131, NULL, 0, 1, 1.00, 0.00, CURDATE(), NULL, NULL, '', NULL);
INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`)
VALUES
@ -1002,7 +1003,8 @@ VALUES
(14, 5, 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 'VT', 1, NULL, NULL, 0, NULL, 0),
(15, 4, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 'EMB', 0, NULL, NULL, 0, NULL, 0),
(16, 6, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 'EMB', 0, NULL, NULL, 0, NULL, 0),
(71, 6, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 'VT', 0, NULL, NULL, 0, NULL, 0);
(71, 6, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 'VT', 0, NULL, NULL, 0, NULL, 0),
(88, 1, 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '', 'Stark Industries', 10.0, 'VT', 0, NULL, NULL, 1, NULL, 0);
-- Update the taxClass after insert of the items
@ -1125,7 +1127,8 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric
(39, 1, 32, 'Ranged weapon longbow 200cm', 2, 103.49, 0, 0, 0, util.VN_CURDATE(), 'hasComponentLack'),
(40, 2, 34, 'Melee weapon combat fist 15cm', 10.00, 3.91, 0, 0, 0, util.VN_CURDATE(), 'hasComponentLack,hasItemLost'),
(41, 2, 35, 'Melee weapon combat fist 15cm', 8.00, 3.01, 0, 0, 0, util.VN_CURDATE(), 'hasComponentLack,hasRounding,hasItemLost'),
(42, 2, 36, 'Melee weapon combat fist 15cm', 6.00, 2.50, 0, 0, 0, util.VN_CURDATE(), 'hasComponentLack,hasRounding,hasItemLost');
(42, 2, 36, 'Melee weapon combat fist 15cm', 6.00, 2.50, 0, 0, 0, util.VN_CURDATE(), 'hasComponentLack,hasRounding,hasItemLost'),
(43, 88, 1000000, 'Chest medical box 2', 15.00, 10.00, 0, 0, 0, CURDATE(), '');
INSERT INTO `vn`.`saleComponent`(`saleFk`, `componentFk`, `value`)
VALUES
@ -1457,7 +1460,14 @@ INSERT INTO `vn`.`itemTag`(`id`,`itemFk`,`tagFk`,`value`,`priority`)
(98, 14, 23, '1', 7),
(99, 15, 92, 'Trolley', 2),
(100, 16, 92, 'Pallet', 2),
(101, 71, 92, 'Shipping cost', 2);
(101, 71, 92, 'Shipping cost', 2),
(102, 88, 56, 'Chest', 1),
(103, 88, 58, 'ammo box', 2),
(104, 88, 27, '100cm', 3),
(105, 88, 36, 'Stark Industries', 4),
(106, 88, 1, 'White', 5),
(107, 88, 67, 'supply', 6),
(108, 88, 23, '13', 7);
INSERT INTO `vn`.`itemTypeTag`(`id`, `itemTypeFk`, `tagFk`, `priority`)
VALUES
@ -1585,7 +1595,8 @@ INSERT INTO `bs`.`waste`(`buyerFk`, `year`, `week`, `itemFk`, `itemTypeFk`, `sal
(15, 7, 4, 1.25, 0, 3, 1, 2.000, 2.000, 0.000, 10, 10, 'grouping', NULL, 0.00, 1.75, 1.67, 0, 1, 0, 4, util.VN_CURDATE()),
(16, 99,1,50.0000, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 'packing', NULL, 0.00, 99.60, 99.40, 0, 1, 0, 1.00, '2024-07-30 08:13:51.000'),
(17, 11, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 'packing', NULL, 0.00, 99.6, 99.4, 0, 1, 0, 1, util.VN_CURDATE() - INTERVAL 2 MONTH),
(18, 12, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 'grouping', NULL, 0.00, 99.6, 99.4, 0, 1, 0, 1, util.VN_CURDATE() - INTERVAL 2 MONTH);
(18, 12, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 'grouping', NULL, 0.00, 99.6, 99.4, 0, 1, 0, 1, util.VN_CURDATE() - INTERVAL 2 MONTH),
(10000002, 12, 88, 50.0000, 5000, '4', 1, 1.500, 1.500, 0.000, 1, 1, 'grouping', NULL, 0.00, 99.60, 99.40, 0, 1, 0, 1.00, util.VN_CURDATE() - INTERVAL 2 MONTH);
INSERT INTO `hedera`.`order`(`id`, `date_send`, `customer_id`, `delivery_method_id`, `agency_id`, `address_id`, `company_id`, `note`, `source_app`, `confirmed`,`total`, `date_make`, `first_row_stamp`, `confirm_date`)
VALUES

View File

@ -1,5 +1,16 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`item_getLack`(IN vForce BOOLEAN, IN vDays INT)
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`item_getLack`(
vSelf INT,
vForce BOOLEAN,
vDays INT,
vLongname VARCHAR(255),
vProducerName VARCHAR(255),
vColor VARCHAR(255),
vSize INT,
vOrigen INT,
vLack INT,
vWarehouseFk INT
)
BEGIN
/**
* Calcula una tabla con el máximo negativo visible para cada producto y almacen
@ -47,6 +58,14 @@ BEGIN
WHERE w.isForTicket
AND ic.display
AND it.code != 'GEN'
AND (vSelf IS NULL OR i.id = vSelf)
AND (vLongname IS NULL OR i.name = vLongname)
AND (vProducerName IS NULL OR p.`name` LIKE CONCAT('%', vProducerName, '%'))
AND (vColor IS NULL OR vColor = i.inkFk)
AND (vSize IS NULL OR vSize = i.`size`)
AND (vOrigen IS NULL OR vOrigen = w.id)
AND (vLack IS NULL OR vLack = sub.amount)
AND (vWarehouseFk IS NULL OR vWarehouseFk = w.id)
GROUP BY i.id, w.id
HAVING lack < 0;

View File

@ -82,14 +82,19 @@ BEGIN
AND it.priority = vPriority
LEFT JOIN vn.tag t ON t.id = it.tagFk
LEFT JOIN vn.buy b ON b.id = bu.buyFk
LEFT JOIN vn.itemShelvingStock iss ON iss.itemFk = i.id
AND iss.warehouseFk = vWarehouseFk
LEFT JOIN vn.ink ink ON ink.id = i.tag5
JOIN itemTags its
WHERE a.available > 0
AND (i.typeFk = its.typeFk OR NOT vShowType)
AND i.id <> vSelf
ORDER BY `counter` DESC,
ORDER BY (a.available > 0) DESC,
`counter` DESC,
(t.name = its.name) DESC,
(it.value = its.value) DESC,
(i.tag5 = its.tag5) DESC,
(ink.`showOrder`) DESC,
match5 DESC,
(i.tag6 = its.tag6) DESC,
match6 DESC,

View File

@ -25,9 +25,11 @@ BEGIN
DECLARE vNewSaleFk INT;
DECLARE vFinalPrice DECIMAL(10,2);
DECLARE vIsRequiredTx BOOL DEFAULT NOT @@in_transaction;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
CALL util.tx_rollback(vIsRequiredTx);
RESIGNAL;
END;
@ -129,6 +131,6 @@ BEGIN
VALUES(vItemFk, vNewItemFk, 1)
ON DUPLICATE KEY UPDATE counter = counter + 1;
COMMIT;
CALL util.tx_commit(vIsRequiredTx);
END$$
DELIMITER ;

View File

@ -0,0 +1,6 @@
INSERT IGNORE INTO salix.ACL (model,property,accessType,permission,principalType,principalId)
VALUES
('Ticket','itemLack','READ','ALLOW','ROLE','employee'),
('Ticket','itemLackDetail','READ','ALLOW','ROLE','employee'),
('Ticket','split','WRITE','ALLOW','ROLE','employee'),
('Sale','replaceItem','WRITE','ALLOW','ROLE','employee');

View File

@ -0,0 +1,2 @@
ALTER TABLE vn.ticketConfig ADD lackAlertPrice int(11) DEFAULT 30 NOT NULL COMMENT 'Value to alert when item proposal exceed price';
ALTER TABLE vn.ticketConfig ADD lackScopeDays int(11) DEFAULT 2 NOT NULL COMMENT 'Number of days to look back for ticket with negatives';

View File

@ -234,6 +234,7 @@
"It has been invoiced but the PDF of refund not be generated": "It has been invoiced but the PDF of refund not be generated",
"Cannot add holidays on this day": "Cannot add holidays on this day",
"Cannot send mail": "Cannot send mail",
"This worker already exists": "This worker already exists",
"CONSTRAINT `chkParkingCodeFormat` failed for `vn`.`parking`": "CONSTRAINT `chkParkingCodeFormat` failed for `vn`.`parking`",
"This postcode already exists": "This postcode already exists",
"Original invoice not found": "Original invoice not found",
@ -254,5 +255,7 @@
"Holidays to past days not available": "Holidays to past days not available",
"Incorrect delivery order alert on route": "Incorrect delivery order alert on route: {{ route }} zone: {{ zone }}",
"Ticket has been delivered out of order": "The ticket {{ticket}} of route {{{fullUrl}}} has been delivered out of order.",
"clonedFromTicketWeekly": ", that is a cloned sale from ticket {{ ticketWeekly }}"
"clonedFromTicketWeekly": ", that is a cloned sale from ticket {{ ticketWeekly }}",
"negativeReplaced": "Replaced item [#{{oldItemId}}]({{{oldItemUrl}}}) {{oldItem}} with [#{{newItemId}}]({{{newItemUrl}}}) {{newItem}} from ticket [{{ticketId}}]({{{ticketUrl}}})",
"The tag and priority can't be repeated": "The tag and priority can't be repeated"
}

View File

@ -397,5 +397,6 @@
"Incorrect delivery order alert on route": "Alerta de orden de entrega incorrecta en ruta: {{ route }} zona: {{ zone }}",
"Ticket has been delivered out of order": "El ticket {{ticket}} {{{fullUrl}}} no ha sido entregado en su orden.",
"Price cannot be blank": "El precio no puede estar en blanco",
"clonedFromTicketWeekly": ", que es una linea clonada del ticket {{ticketWeekly}}"
"clonedFromTicketWeekly": ", que es una linea clonada del ticket {{ticketWeekly}}",
"negativeReplaced": "Sustituido el articulo [#{{oldItemId}}]({{{oldItemUrl}}}) {{oldItem}} por [#{{newItemId}}]({{{newItemUrl}}}) {{newItem}} del ticket [{{ticketId}}]({{{ticketUrl}}})"
}

View File

@ -368,5 +368,6 @@
"ticketLostExpedition": "Le ticket [{{ticketId}}]({{{ticketUrl}}}) a l'expédition perdue suivante : {{expeditionId}}",
"The web user's email already exists": "L'email de l'internaute existe déjà",
"Incorrect delivery order alert on route": "Alerte de bon de livraison incorrect sur l'itinéraire: {{ route }} zone : {{ zone }}",
"Ticket has been delivered out of order": "Le ticket {{ticket}} de la route {{{fullUrl}}} a été livré hors service."
"Ticket has been delivered out of order": "Le ticket {{ticket}} de la route {{{fullUrl}}} a été livré hors service.",
"negativeReplaced": "Remplacé l'article [#{{oldItemId}}]({{{oldItemUrl}}}) {{oldItem}} par [#{{newItemId}}]({{{newItemUrl}}}) {{newItem}} du ticket [{{ticketId}}]({{{ticketUrl}}})"
}

View File

@ -367,5 +367,6 @@
"ticketLostExpedition": "O ticket [{{ticketId}}]({{{ticketUrl}}}) tem a seguinte expedição perdida: {{expeditionId}}",
"The web user's email already exists": "O e-mail do utilizador da web já existe.",
"Incorrect delivery order alert on route": "Alerta de ordem de entrega incorreta na rota: {{ route }} zona: {{ zone }}",
"Ticket has been delivered out of order": "O ticket {{ticket}} da rota {{{fullUrl}}} foi entregue fora de ordem."
"Ticket has been delivered out of order": "O ticket {{ticket}} da rota {{{fullUrl}}} foi entregue fora de ordem.",
"negativeReplaced": "Substituído o artigo [#{{oldItemId}}]({{{oldItemUrl}}}) {{oldItem}} por [#{{newItemId}}]({{{newItemUrl}}}) {{newItem}} do ticket [{{ticketId}}]({{{ticketUrl}}})"
}

View File

@ -0,0 +1,43 @@
module.exports = Self => {
Self.remoteMethod('getSimilar', {
description: 'Returns list of items with similar item requested',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'Object',
required: true,
description: 'Filter defining where and paginated data',
http: {source: 'query'}
}
],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/getSimilar`,
verb: 'GET'
}
});
Self.getSimilar = async(ctx, filter, options) => {
const myOptions = {userId: ctx.req.accessToken.userId};
if (typeof options == 'object')
Object.assign(myOptions, options);
const {where} = filter;
const query = [
filter.itemFk,
where.warehouseFk,
where.date,
where.showType,
where.scopeDays
];
const [results] = await Self.rawSql('CALL vn.item_getSimilar(?, ?, ?, ?, ?)', query, myOptions);
return results;
};
};

View File

@ -0,0 +1,49 @@
const models = require('vn-loopback/server/server').models;
describe('Item get similar', () => {
let options;
let tx;
const ctx = beforeAll.getCtx();
beforeAll.mockLoopBackContext();
beforeEach(async() => {
tx = await models.Item.beginTransaction({});
options = {transaction: tx};
});
afterEach(async() => {
if (tx)
await tx.rollback();
});
it('should return similar items', async() => {
const filter = {
itemFk: 88, sales: 43,
where: {
'scopeDays': '2',
'showType': true,
'alertLevelCode': 'FREE',
'date': '2001-01-01T11:00:00.000Z',
'warehouseFk': 1
}
};
const result = await models.Item.getSimilar(ctx, filter, options);
expect(result.length).toEqual(2);
});
it('should return empty array is if not exists', async() => {
const filter = {
itemFk: 88, sales: 43,
where: {
'scopeDays': '2',
'showType': true,
'alertLevelCode': 'FREE',
'date': '2001-01-01T11:00:00.000Z',
'warehouseFk': 60
}
};
const result = await models.Item.getSimilar(ctx, filter, options);
expect(result.length).toEqual(0);
});
});

View File

@ -5,6 +5,7 @@ module.exports = Self => {
require('../methods/item/clone')(Self);
require('../methods/item/updateTaxes')(Self);
require('../methods/item/getBalance')(Self);
require('../methods/item/getSimilar')(Self);
require('../methods/item/lastEntriesFilter')(Self);
require('../methods/item/getSummary')(Self);
require('../methods/item/getCard')(Self);

View File

@ -31,7 +31,7 @@ describe('route getSuggestedTickets()', () => {
const length = result.length;
const anyResult = result[Math.floor(Math.random() * Math.floor(length))];
expect(result.length).toEqual(4);
expect(result.length).toEqual(5);
expect(anyResult.zoneFk).toEqual(1);
expect(anyResult.agencyModeFk).toEqual(8);

View File

@ -14,7 +14,7 @@ describe('route unlink()', () => {
let tickets = await models.Route.getSuggestedTickets(routeId, options);
expect(zoneAgencyModes.length).toEqual(4);
expect(tickets.length).toEqual(3);
expect(tickets.length).toEqual(4);
await models.Route.unlink(agencyModeId, zoneId, options);

View File

@ -0,0 +1,99 @@
module.exports = Self => {
Self.remoteMethodCtx('replaceItem', {
description: 'Replace item from sale',
accessType: 'WRITE',
accepts: [
{
arg: 'saleFk',
type: 'number',
required: true,
},
{
arg: 'substitutionFk',
type: 'number',
required: true
},
{
arg: 'quantity',
type: 'number',
required: true
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/replaceItem`,
verb: 'POST'
}
});
Self.replaceItem = async(ctx, saleFk, substitutionFk, quantity, options) => {
const myOptions = {userId: ctx.req.accessToken.userId};
let tx;
const $t = ctx.req.__;
const models = Self.app.models;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const replaceItemQuery = {
sql: 'CALL sale_replaceItem(?,?,?)',
query: [saleFk, substitutionFk, quantity]
};
const resultReplaceItem = await Self.rawSql(replaceItemQuery.sql, replaceItemQuery.query, myOptions);
const sale = await models.Sale.findById(saleFk, {
fields: ['id', 'ticketFk', 'itemFk', 'quantity', 'price'],
include: [
{
relation: 'ticket',
scope: {
fields: ['id']
},
}, {
relation: 'item',
scope: {
fields: ['id', 'name', 'longName']
}
}
]
}, myOptions);
const salesPersonQuery = {
sql: 'SELECT vn.client_getSalesPersonByTicket(?)',
query: [sale.ticketFk]
};
const salesPerson = await Self.rawSql(salesPersonQuery.sql, salesPersonQuery.query, myOptions);
const url = await models.Url.getUrl();
const substitution = await models.Item.findById(substitutionFk, {
fields: ['id', 'name', 'longName']
}, myOptions);
const message = $t('negativeReplaced', {
oldItemId: sale.itemFk,
oldItem: sale.item().longName,
oldItemUrl: `${url}item/${sale.itemFk}/summary`,
newItemId: substitution.id,
newItem: substitution.longName,
newItemUrl: `${url}item/${substitution.id}/summary`,
ticketId: sale.ticketFk,
ticketUrl: `${url}ticket/${sale.ticketFk}/sale`
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
return resultReplaceItem;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,61 @@
const {models} = require('vn-loopback/server/server');
describe('Sale - replaceItem function', () => {
let options;
let tx;
const ctx = beforeAll.getCtx();
beforeAll.mockLoopBackContext();
beforeEach(async() => {
tx = await models.Sale.beginTransaction({});
options = {transaction: tx};
});
afterEach(async() => {
if (tx)
await tx.rollback();
});
it('should replace full item in sale and send notification', async() => {
const saleFk = 43;
const substitutionFk = 3;
const quantity = 15;
const ticketFk = 1000000;
const salesBefore = await models.Sale.find({where: {ticketFk}}, options);
const salesLength = salesBefore.length;
expect(1).toEqual(salesBefore.length);
await models.Sale.replaceItem(ctx, saleFk, substitutionFk, quantity, options);
const salesAfter = await models.Sale.find({where: {ticketFk}}, options);
expect(salesLength).toBeLessThan(salesAfter.length);
expect(salesAfter[0].id).toEqual(saleFk);
expect(salesAfter[salesLength].itemFk).toEqual(substitutionFk);
expect(salesAfter[salesLength].quantity).toEqual(quantity);
expect(salesAfter[0].quantity).toEqual(0);
expect(salesAfter[salesLength].concept).toMatch(/^\+/);
});
it('should replace half item in sale and send notification', async() => {
const saleFk = 43;
const substitutionFk = 3;
const quantity = 10;
const ticketFk = 1000000;
const salesBefore = await models.Sale.find({where: {ticketFk}}, options);
const salesLength = salesBefore.length;
expect(1).toEqual(salesBefore.length);
await models.Sale.replaceItem(ctx, saleFk, substitutionFk, quantity, options);
const salesAfter = await models.Sale.find({where: {ticketFk}}, options);
expect(salesLength).toBeLessThan(salesAfter.length);
expect(salesAfter[0].id).toEqual(saleFk);
expect(salesAfter[salesLength].itemFk).toEqual(substitutionFk);
expect(salesAfter[salesLength].quantity).toEqual(quantity);
expect(salesAfter[0].quantity).toEqual(5);
expect(salesAfter[salesLength].concept).toMatch(/^\+/);
});
});

View File

@ -0,0 +1,111 @@
module.exports = Self => {
Self.remoteMethod('itemLack', {
description: 'Get tickets as negative status',
accessType: 'READ',
accepts: [
{
arg: 'ctx',
type: 'object',
http: {source: 'context'}
},
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
},
{
arg: 'id',
type: 'number',
description: 'The item id',
},
{
arg: 'longname',
type: 'string',
description: 'Article name',
},
{
arg: 'supplier',
type: 'string',
description: 'Supplier id',
},
{
arg: 'colour',
type: 'string',
description: 'Colour\'s item',
},
{
arg: 'size',
type: 'string',
description: 'Size\'s item',
},
{
arg: 'origen',
type: 'string',
description: 'origen id',
},
{
arg: 'warehouseFk',
type: 'number',
description: 'The warehouse id',
},
{
arg: 'lack',
type: 'number',
description: 'The item id',
},
{
arg: 'days',
type: 'number',
description: 'The range days',
}
],
returns: [
{
arg: 'body',
type: ['object'],
root: true
}
],
http: {
path: `/itemLack`,
verb: 'GET'
}
});
Self.itemLack = async(ctx, filter, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const filterKeyOrder = [
'id', 'force', 'days', 'longname', 'supplier',
'colour', 'size', 'originFk',
'lack', 'warehouseFk'
];
delete ctx?.args?.ctx;
delete ctx?.args?.filter;
Object.assign(filter, ctx.args ?? {});
let procedureParams = [];
procedureParams.push(...filterKeyOrder.map(clave => filter[clave] ?? null));
// Default values
const forceIndex = filterKeyOrder.indexOf('force');
if (!procedureParams[forceIndex])procedureParams[forceIndex] = true;
const daysIndex = filterKeyOrder.indexOf('days');
if (!procedureParams[daysIndex])procedureParams[daysIndex] = 2;
const procedureArgs = Array(procedureParams.length).fill('?').join(', ');
let query = `CALL vn.item_getLack(${procedureArgs})`;
const result = await Self.rawSql(query, procedureParams, myOptions);
const itemsIndex = 0;
return result[itemsIndex];
};
};

View File

@ -0,0 +1,167 @@
const {ParameterizedSQL} = require('loopback-connector');
module.exports = Self => {
Self.remoteMethod('itemLackDetail', {
description: 'Retrieve detail from ticket as negative',
accessType: 'READ',
accepts: [
{
arg: 'itemFk',
type: 'number',
description: 'The item as negative status',
},
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
}
],
returns: [
{
arg: 'body',
type: ['object'],
root: true,
},
],
http: {
path: `/itemLack/:itemFk`,
verb: 'GET',
},
});
Self.itemLackDetail = async(itemFk, filter, options) => {
const conn = Self.dataSource.connector;
const myOptions = {};
if (typeof options == 'object') Object.assign(myOptions, options);
const vDated = (Date.vnNew());
vDated.setHours(0, 0, 0, 0);
const scopeDays = filter.where.scopeDays ?? 0;
let alertLevels = filter.where.alertLevelCode;
if (!alertLevels)
alertLevels = (await Self.app.models.AlertLevel.find({fields: ['code']})).map(({code}) => code);
const stmt = new ParameterizedSQL(`
SELECT s.id,
st.code,
t.id,
t.nickname,
c.id customerId,
t.shipped,
s.quantity,
ag.name,
ag.id agencyFk,
tls.alertLevel alertLevel,
st.name stateName,
s.id saleFk,
s.itemFk,
s.price price,
al.code alertLevelCode,
z.name zoneName,
z.id zoneFk,
z.hour theoreticalhour,
cn.isRookie,
sc.saleClonedFk turno,
tr.saleFk peticionCompra,
DATE_FORMAT(IF(HOUR(t.shipped), t.shipped, IF(zc.hour, zc.hour, z.hour)),'%H:%i') minTimed,
FALSE isBasket,
substitution.hasObservation,
(d.code = 'spainTeamVip') hasToIgnore
FROM sale s
LEFT JOIN saleGroupDetail sgd ON sgd.saleFk = s.id
JOIN ticket t ON t.id = s.ticketFk
LEFT JOIN zone z ON z.id = t.zoneFk
LEFT JOIN zoneClosure zc ON zc.zoneFk = t.zoneFk
AND t.shipped BETWEEN zc.dated AND util.dayEnd(t.shipped)
JOIN client c ON c.id=t.clientFk
LEFT JOIN bs.clientNewBorn cn ON cn.clientFk=c.id
JOIN agencyMode ag ON ag.id=t.agencyModeFk
JOIN ticketState tls ON tls.ticketFk=t.id
LEFT JOIN state st ON st.id=tls.state
LEFT JOIN alertLevel al ON al.id = st.alertLevel
LEFT JOIN saleCloned sc ON sc.saleClonedFk = s.id
LEFT JOIN ticketRequest tr ON tr.saleFk = s.id
LEFT JOIN workerDepartment wd ON wd.workerFk = c.salesPersonFk
LEFT JOIN department d ON d.id = wd.departmentFk
LEFT JOIN (
SELECT co.clientFk, COUNT(*) hasObservation
FROM clientObservation co
JOIN observationType ot ON ot.id = co.observationTypeFk
WHERE ot.code = 'substitution'
GROUP BY co.clientFk
) substitution ON substitution.clientFk = c.id
WHERE t.warehouseFk = ?
AND s.itemFk = ?
AND s.quantity <> 0
AND t.shipped BETWEEN ? AND (? + INTERVAL ? DAY)
AND sgd.saleFk IS NULL
AND (al.code IN (?) OR al.id IS NULL)
UNION ALL
SELECT r.id,
NULL,
r.orderFk,
c.name customerName,
c.id customerId,
r.shipment,
r.amount,
ag.name,
ag.id,
NULL,
NULL,
NULL,
r.itemFk,
NULL,
NULL,
NULL,
NULL,
NULL,
cn.isRookie,
NULL,
NULL,
NULL,
TRUE,
substitution.hasObservation,
d.code = 'spainTeamVip'
FROM hedera.orderRow r
JOIN hedera.order o ON o.id = r.orderFk
JOIN client c ON c.id = o.customer_id
JOIN agencyMode ag ON ag.id=o.agency_id
LEFT JOIN bs.clientNewBorn cn ON cn.clientFk=c.id
LEFT JOIN workerDepartment wd ON wd.workerFk = c.salesPersonFk
LEFT JOIN department d ON d.id = wd.departmentFk
LEFT JOIN (
SELECT co.clientFk, COUNT(*) hasObservation
FROM clientObservation co
JOIN observationType ot ON ot.id = co.observationTypeFk
WHERE ot.code = 'substitution'
GROUP BY co.clientFk
) substitution ON substitution.clientFk = c.id
WHERE r.shipment BETWEEN ? AND ? + INTERVAL ? DAY
AND r.created >= ?
AND r.warehouseFk = ?
AND NOT o.confirmed
AND r.itemFk = ?
AND r.amount
ORDER BY hasToIgnore, isBasket
`,
[
filter.where.warehouseFk,
itemFk,
vDated, vDated,
scopeDays,
alertLevels,
scopeDays,
vDated, vDated, vDated,
filter.where.warehouseFk,
itemFk
]);
const sql = ParameterizedSQL.join([stmt], ';');
const result = await conn.executeStmt(sql, myOptions);
return result;
};
};

View File

@ -0,0 +1,80 @@
const {models} = require('vn-loopback/server/server');
describe('Item Lack', () => {
let options;
let tx;
const ctx = beforeAll.getCtx();
beforeAll.mockLoopBackContext();
beforeEach(async() => {
tx = await models.Ticket.beginTransaction({});
options = {transaction: tx};
});
afterEach(async() => {
if (tx)
await tx.rollback();
});
it('should return data with NO filters', async() => {
const filter = {};
const result = await models.Ticket.itemLack(ctx, filter, options);
expect(result.length).toEqual(2);
});
it('should return data with filter.id', async() => {
const filter = {
id: 5
};
const result = await models.Ticket.itemLack(ctx, filter, options);
expect(result.length).toEqual(1);
});
it('should return data with filter.longname', async() => {
const filter = {
longname: 'Ranged weapon pistol 9mm'
};
const result = await models.Ticket.itemLack(ctx, filter, options);
expect(result.length).toEqual(1);
});
it('should return data with filter.color', async() => {
const filter = {
colour: 'WHT'
};
const result = await models.Ticket.itemLack(ctx, filter, options);
expect(result.length).toEqual(1);
});
it('should return data with filter.origen', async() => {
const filter = {
originFk: 1
};
const result = await models.Ticket.itemLack(ctx, filter, options);
expect(result.length).toEqual(2);
});
it('should return data with filter.size', async() => {
const filter = {
size: '15'
};
const result = await models.Ticket.itemLack(ctx, filter, options);
expect(result.length).toEqual(1);
});
it('should return data with filter.lack', async() => {
const filter = {
lack: '-15'
};
const result = await models.Ticket.itemLack(ctx, filter, options);
expect(result.length).toEqual(1);
});
});

View File

@ -0,0 +1,55 @@
const models = require('vn-loopback/server/server').models;
describe('Item Lack Detail', () => {
it('should return false if id is null', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const itemFk = null;
const filter = {where: {warehouseFk: 60}};
const result = await models.Ticket.itemLackDetail(itemFk, filter, options);
expect(result.length).toEqual(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return data if id exists', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const itemFk = 1167;
const filter = {where: {warehouseFk: 60}};
const result = await models.Ticket.itemLackDetail(itemFk, filter, options);
expect(result.length).toEqual(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return error is if not exists', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const itemFk = 0;
const filter = {where: {warehouseFk: 60}};
const result = await models.Ticket.itemLackDetail(itemFk, filter, options);
expect(result.length).toEqual(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,47 @@
const {models} = require('vn-loopback/server/server');
describe('Split', () => {
let options;
let tx;
const ctx = beforeAll.getCtx();
beforeAll.mockLoopBackContext();
beforeEach(async() => {
tx = await models.Ticket.beginTransaction({});
options = {transaction: tx};
});
afterEach(async() => {
if (tx)
await tx.rollback();
});
it('should split tickets with count 1', async() => {
const data =
{ticketFk: 7, sales: [1]};
const result = await models.Ticket.split(ctx, data, options);
expect(data.ticketFk).toEqual(result.ticket);
expect('noSplit').toEqual(result.status);
});
it('should split tickets with count 2 and error', async() => {
const data =
{ticketFk: 11, sales: [7]}
;
const result = await models.Ticket.split(ctx, data, options);
expect(data.ticketFk).toEqual(result.ticket);
expect('error').toEqual(result.status);
expect('Can\'t transfer claimed sales').toEqual(result.message);
});
it('should split tickets with count 2 and success', async() => {
const data =
{ticketFk: 14, sales: [33]};
const result = await models.Ticket.split(ctx, data, options);
expect(data.ticketFk).toEqual(result.ticket);
expect('split').toEqual(result.status);
});
});

View File

@ -0,0 +1,73 @@
module.exports = Self => {
Self.remoteMethodCtx('split', {
description: 'Split ticket with custom date',
accessType: 'WRITE',
accepts: [
{
arg: 'ticket',
type: 'Object',
required: true,
http: {source: 'body'}
},
{
arg: 'date',
type: 'date',
required: true,
}
],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/split`,
verb: 'POST'
}
});
Self.split = async(ctx, ticket, options) => {
const {ticketFk} = ticket;
const models = Self.app.models;
const myOptions = {};
let tx;
let result = [];
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const count = await models.Sale.count({
ticketFk
}, myOptions);
if (count === 1)
return {ticket: ticketFk, status: 'noSplit'};
const [, [{vNewTicket}]] = await Self.rawSql(`
CALL vn.ticket_clone(?, @vNewTicket);
SELECT @vNewTicket vNewTicket;`,
[ticketFk], myOptions);
if (vNewTicket === 0) return result;
const sales = await models.Sale.find({
where: {id: {inq: ticket.sales}}
}, myOptions);
const updateIsPicked = sales.map(({sid}) => Self.rawSql(`
UPDATE vn.sale SET isPicked = (id = ?) WHERE ticketFk = ?`,
[sid, ticketFk], myOptions));
await Promise.all(updateIsPicked);
await Self.transferSales(ctx, ticketFk, vNewTicket, sales, myOptions);
await Self.rawSql(`CALL vn.ticket_setState(?, ?)`, [ticketFk, 'FIXING'], myOptions);
if (tx) await tx.commit();
return {ticket: ticketFk, newTicket: vNewTicket, status: 'split'};
} catch (e) {
if (tx) await tx.rollback();
return {ticket: ticketFk, status: 'error', message: e.message};
}
};
};

View File

@ -43,8 +43,8 @@ module.exports = Self => {
const {code} = await models.State.findById(params.stateFk, {fields: ['code']}, myOptions);
params.code = code;
} else {
const {id} = await models.State.findOne({where: {code: params.code}}, myOptions);
params.stateFk = id;
const state = await models.State.findOne({where: {id: params.code}}, myOptions);
params.stateFk = state.id;
}
if (!params.userFk) {

View File

@ -13,6 +13,7 @@ module.exports = Self => {
require('../methods/sale/usesMana')(Self);
require('../methods/sale/clone')(Self);
require('../methods/sale/getFromSectorCollection')(Self);
require('../methods/sale/replaceItem')(Self);
Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank`

View File

@ -26,6 +26,12 @@
},
"defaultAttenderFk": {
"type": "number"
},
"lackAlertPrice": {
"type": "number"
},
"lackScopeDays": {
"type": "number"
}
},
"relations": {

View File

@ -46,5 +46,8 @@ module.exports = function(Self) {
require('../methods/ticket/docuwareDownload')(Self);
require('../methods/ticket/myLastModified')(Self);
require('../methods/ticket/setWeight')(Self);
require('../methods/ticket/itemLack')(Self);
require('../methods/ticket/itemLackDetail')(Self);
require('../methods/ticket/split')(Self);
require('../methods/ticket/getTicketProblems')(Self);
};