diff --git a/modules/item/front/summary/locale/es.yml b/modules/item/front/summary/locale/es.yml
index 2e78841ae..80988c491 100644
--- a/modules/item/front/summary/locale/es.yml
+++ b/modules/item/front/summary/locale/es.yml
@@ -2,3 +2,4 @@ Barcode: Códigos de barras
Other data: Otros datos
Go to the item: Ir al artículo
WarehouseFk: Calculado sobre el almacén de {{ warehouseName }}
+Minimum sales quantity: Cantidad mínima de venta
diff --git a/modules/order/back/methods/order/catalogFilter.js b/modules/order/back/methods/order/catalogFilter.js
index 0d83f9f4a..722f3e096 100644
--- a/modules/order/back/methods/order/catalogFilter.js
+++ b/modules/order/back/methods/order/catalogFilter.js
@@ -100,31 +100,32 @@ module.exports = Self => {
));
stmt = new ParameterizedSQL(`
- SELECT
- i.id,
- i.name,
- i.subName,
- i.image,
- i.tag5,
- i.value5,
- i.tag6,
- i.value6,
- i.tag7,
- i.value7,
- i.tag8,
- i.value8,
- i.stars,
- tci.price,
- tci.available,
- w.lastName AS lastName,
- w.firstName,
- tci.priceKg,
- ink.hex
+ SELECT i.id,
+ i.name,
+ i.subName,
+ i.image,
+ i.tag5,
+ i.value5,
+ i.tag6,
+ i.value6,
+ i.tag7,
+ i.value7,
+ i.tag8,
+ i.value8,
+ i.stars,
+ tci.price,
+ tci.available,
+ w.lastName,
+ w.firstName,
+ tci.priceKg,
+ ink.hex,
+ i.minQuantity
FROM tmp.ticketCalculateItem tci
JOIN vn.item i ON i.id = tci.itemFk
JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.worker w on w.id = it.workerFk
- LEFT JOIN vn.ink ON ink.id = i.inkFk`);
+ LEFT JOIN vn.ink ON ink.id = i.inkFk
+ `);
// Apply order by tag
if (orderBy.isTag) {
diff --git a/modules/order/front/catalog-view/index.html b/modules/order/front/catalog-view/index.html
index fca728855..5d60211ed 100644
--- a/modules/order/front/catalog-view/index.html
+++ b/modules/order/front/catalog-view/index.html
@@ -8,12 +8,12 @@
-
{{::item.name}}
@@ -37,13 +37,28 @@
value="{{::item.value7}}">
-
-
+
+
+
+
+
+
+
+
+
+ {{::item.minQuantity}}
+
+
@@ -69,4 +84,4 @@
-
\ No newline at end of file
+
diff --git a/modules/order/front/catalog-view/locale/es.yml b/modules/order/front/catalog-view/locale/es.yml
index 82fe5e9e8..8fb3c7896 100644
--- a/modules/order/front/catalog-view/locale/es.yml
+++ b/modules/order/front/catalog-view/locale/es.yml
@@ -1 +1,2 @@
Order created: Orden creada
+Minimal quantity: Cantidad mínima
\ No newline at end of file
diff --git a/modules/order/front/catalog-view/style.scss b/modules/order/front/catalog-view/style.scss
index 87f70cde5..1e48745ca 100644
--- a/modules/order/front/catalog-view/style.scss
+++ b/modules/order/front/catalog-view/style.scss
@@ -44,4 +44,7 @@ vn-order-catalog {
height: 30px;
position: relative;
}
-}
\ No newline at end of file
+ .alert {
+ color: $color-alert;
+ }
+}
diff --git a/modules/ticket/back/methods/sale/refund.js b/modules/ticket/back/methods/sale/refund.js
index 67172f3ac..03302550e 100644
--- a/modules/ticket/back/methods/sale/refund.js
+++ b/modules/ticket/back/methods/sale/refund.js
@@ -55,7 +55,7 @@ module.exports = Self => {
const refoundZoneId = refundAgencyMode.zones()[0].id;
- if (salesIds) {
+ if (salesIds.length) {
const salesFilter = {
where: {id: {inq: salesIds}},
include: {
@@ -91,16 +91,14 @@ module.exports = Self => {
await models.SaleComponent.create(components, myOptions);
}
}
-
if (!refundTicket) {
const servicesFilter = {
where: {id: {inq: servicesIds}}
};
const services = await models.TicketService.find(servicesFilter, myOptions);
- const ticketsIds = [...new Set(services.map(service => service.ticketFk))];
+ const firstTicketId = services[0].ticketFk;
const now = Date.vnNew();
- const [firstTicketId] = ticketsIds;
// eslint-disable-next-line max-len
refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions);
diff --git a/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js b/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js
index 8064ea30b..0fde997fa 100644
--- a/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js
+++ b/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js
@@ -1,21 +1,9 @@
+/* eslint max-len: ["error", { "code": 150 }]*/
+
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale updateQuantity()', () => {
- beforeAll(async() => {
- const activeCtx = {
- accessToken: {userId: 9},
- http: {
- req: {
- headers: {origin: 'http://localhost'}
- }
- }
- };
- spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
- active: activeCtx
- });
- });
-
const ctx = {
req: {
accessToken: {userId: 9},
@@ -23,6 +11,18 @@ describe('sale updateQuantity()', () => {
__: () => {}
}
};
+ function getActiveCtx(userId) {
+ return {
+ active: {
+ accessToken: {userId},
+ http: {
+ req: {
+ headers: {origin: 'http://localhost'}
+ }
+ }
+ }
+ };
+ }
it('should throw an error if the quantity is greater than it should be', async() => {
const ctx = {
@@ -32,13 +32,16 @@ describe('sale updateQuantity()', () => {
__: () => {}
}
};
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
+ spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: 100}))));
+
const tx = await models.Sale.beginTransaction({});
let error;
try {
const options = {transaction: tx};
- await models.Sale.updateQuantity(ctx, 17, 99, options);
+ await models.Sale.updateQuantity(ctx, 17, 31, options);
await tx.rollback();
} catch (e) {
@@ -50,7 +53,6 @@ describe('sale updateQuantity()', () => {
});
it('should add quantity if the quantity is greater than it should be and is role advanced', async() => {
- const tx = await models.Sale.beginTransaction({});
const saleId = 17;
const buyerId = 35;
const ctx = {
@@ -60,6 +62,9 @@ describe('sale updateQuantity()', () => {
__: () => {}
}
};
+ const tx = await models.Sale.beginTransaction({});
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(buyerId));
+ spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: 100}))));
try {
const options = {transaction: tx};
@@ -87,6 +92,8 @@ describe('sale updateQuantity()', () => {
});
it('should update the quantity of a given sale current line', async() => {
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(9));
+
const tx = await models.Sale.beginTransaction({});
const saleId = 25;
const newQuantity = 4;
@@ -119,6 +126,8 @@ describe('sale updateQuantity()', () => {
__: () => {}
}
};
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
+
const saleId = 17;
const newQuantity = -10;
@@ -140,6 +149,8 @@ describe('sale updateQuantity()', () => {
});
it('should update a negative quantity when is a ticket refund', async() => {
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(9));
+
const tx = await models.Sale.beginTransaction({});
const saleId = 13;
const newQuantity = -10;
@@ -159,4 +170,70 @@ describe('sale updateQuantity()', () => {
throw e;
}
});
+
+ it('should throw an error if the quantity is less than the minimum quantity of the item', async() => {
+ const ctx = {
+ req: {
+ accessToken: {userId: 1},
+ headers: {origin: 'localhost:5000'},
+ __: () => {}
+ }
+ };
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
+
+ const tx = await models.Sale.beginTransaction({});
+ const itemId = 2;
+ const saleId = 17;
+ const minQuantity = 30;
+ const newQuantity = minQuantity - 1;
+
+ let error;
+ try {
+ const options = {transaction: tx};
+
+ const item = await models.Item.findById(itemId, null, options);
+ await item.updateAttribute('minQuantity', minQuantity, options);
+
+ await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ error = e;
+ }
+
+ expect(error).toEqual(new Error('The amount cannot be less than the minimum'));
+ });
+
+ it('should change quantity if has minimum quantity and new quantity is equal than item available', async() => {
+ const ctx = {
+ req: {
+ accessToken: {userId: 1},
+ headers: {origin: 'localhost:5000'},
+ __: () => {}
+ }
+ };
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
+
+ const tx = await models.Sale.beginTransaction({});
+ const itemId = 2;
+ const saleId = 17;
+ const minQuantity = 30;
+ const newQuantity = minQuantity - 1;
+
+ try {
+ const options = {transaction: tx};
+
+ const item = await models.Item.findById(itemId, null, options);
+ await item.updateAttribute('minQuantity', minQuantity, options);
+ spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: newQuantity}))));
+
+ await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
});
diff --git a/modules/ticket/back/methods/sale/updateQuantity.js b/modules/ticket/back/methods/sale/updateQuantity.js
index 28754e50c..1a497da24 100644
--- a/modules/ticket/back/methods/sale/updateQuantity.js
+++ b/modules/ticket/back/methods/sale/updateQuantity.js
@@ -1,4 +1,3 @@
-let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('updateQuantity', {
@@ -64,17 +63,6 @@ module.exports = Self => {
const sale = await models.Sale.findById(id, filter, myOptions);
- const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*');
- if (newQuantity > sale.quantity && !isRoleAdvanced)
- throw new UserError('The new quantity should be smaller than the old one');
-
- const ticketRefund = await models.TicketRefund.findOne({
- where: {refundTicketFk: sale.ticketFk},
- fields: ['id']}
- , myOptions);
- if (newQuantity < 0 && !ticketRefund)
- throw new UserError('You can only add negative amounts in refund tickets');
-
const oldQuantity = sale.quantity;
const result = await sale.updateAttributes({quantity: newQuantity}, myOptions);
diff --git a/modules/ticket/back/methods/ticket/addSale.js b/modules/ticket/back/methods/ticket/addSale.js
index 59f1190fa..3455ec2c4 100644
--- a/modules/ticket/back/methods/ticket/addSale.js
+++ b/modules/ticket/back/methods/ticket/addSale.js
@@ -63,17 +63,6 @@ module.exports = Self => {
}
}, myOptions);
- const itemInfo = await models.Item.getVisibleAvailable(
- itemId,
- ticket.warehouseFk,
- ticket.shipped,
- myOptions
- );
-
- const isPackaging = item.family == 'EMB';
- if (!isPackaging && itemInfo.available < quantity)
- throw new UserError(`This item is not available`);
-
const newSale = await models.Sale.create({
ticketFk: id,
itemFk: item.id,
diff --git a/modules/ticket/back/models/sale.js b/modules/ticket/back/models/sale.js
index ae247fc24..fe6307270 100644
--- a/modules/ticket/back/models/sale.js
+++ b/modules/ticket/back/models/sale.js
@@ -1,3 +1,6 @@
+const UserError = require('vn-loopback/util/user-error');
+const LoopBackContext = require('loopback-context');
+
module.exports = Self => {
require('../methods/sale/getClaimableFromTicket')(Self);
require('../methods/sale/reserve')(Self);
@@ -13,4 +16,77 @@ module.exports = Self => {
Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank`
});
+
+ Self.observe('before save', async ctx => {
+ const models = Self.app.models;
+ const changes = ctx.data || ctx.instance;
+ const instance = ctx.currentInstance;
+
+ const newQuantity = changes?.quantity;
+ if (newQuantity == null) return;
+
+ const loopBackContext = LoopBackContext.getCurrentContext();
+ ctx.req = loopBackContext.active;
+ if (await models.ACL.checkAccessAcl(ctx, 'Sale', 'canForceQuantity', 'WRITE')) return;
+
+ const ticketId = changes?.ticketFk || instance?.ticketFk;
+ const itemId = changes?.itemFk || instance?.itemFk;
+
+ const ticket = await models.Ticket.findById(
+ ticketId,
+ {
+ fields: ['id', 'clientFk', 'warehouseFk', 'shipped'],
+ include: {
+ relation: 'client',
+ scope: {
+ fields: ['id', 'clientTypeFk'],
+ include: {
+ relation: 'type',
+ scope: {
+ fields: ['code', 'description']
+ }
+ }
+ }
+ }
+ },
+ ctx.options);
+ if (ticket?.client()?.type()?.code === 'loses') return;
+
+ const isRefund = await models.TicketRefund.findOne({
+ fields: ['id'],
+ where: {refundTicketFk: ticketId}
+ }, ctx.options);
+ if (isRefund) return;
+
+ if (newQuantity < 0)
+ throw new UserError('You can only add negative amounts in refund tickets');
+
+ const item = await models.Item.findOne({
+ fields: ['family', 'minQuantity'],
+ where: {id: itemId},
+ }, ctx.options);
+
+ if (item.family == 'EMB') return;
+
+ const itemInfo = await models.Item.getVisibleAvailable(
+ itemId,
+ ticket.warehouseFk,
+ ticket.shipped,
+ ctx.options
+ );
+
+ const oldQuantity = instance?.quantity ?? null;
+ const quantityAdded = newQuantity - oldQuantity;
+ if (itemInfo.available < quantityAdded)
+ throw new UserError(`This item is not available`);
+
+ if (await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*')) return;
+
+ if (newQuantity < item.minQuantity && itemInfo.available != newQuantity)
+ throw new UserError('The amount cannot be less than the minimum');
+
+ if (!ctx.isNewInstance && newQuantity > oldQuantity)
+ throw new UserError('The new quantity should be smaller than the old one');
+ });
};
+
diff --git a/modules/ticket/front/index/locale/es.yml b/modules/ticket/front/index/locale/es.yml
index afa3d654e..89828d4d9 100644
--- a/modules/ticket/front/index/locale/es.yml
+++ b/modules/ticket/front/index/locale/es.yml
@@ -18,3 +18,4 @@ Multiple invoice: Factura múltiple
Make invoice...: Crear factura...
Invoice selected tickets: Facturar tickets seleccionados
Are you sure to invoice tickets: ¿Seguro que quieres facturar {{ticketsAmount}} tickets?
+Rounding: Redondeo
diff --git a/modules/ticket/front/sale/index.html b/modules/ticket/front/sale/index.html
index be9e81964..b8e64cf28 100644
--- a/modules/ticket/front/sale/index.html
+++ b/modules/ticket/front/sale/index.html
@@ -318,7 +318,7 @@
clear-disabled="true"
suffix="%">