Merge branch 'test' into dev
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Guillermo Bonet 2023-10-19 13:22:33 +02:00
commit 5c51d7fb04
34 changed files with 324 additions and 218 deletions

View File

@ -18,6 +18,7 @@ describe('setSaleQuantity()', () => {
it('should change quantity sale', async() => { it('should change quantity sale', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: 100}))));
try { try {
const options = {transaction: tx}; const options = {transaction: tx};

View File

@ -30434,6 +30434,7 @@ CREATE TABLE `item` (
`editorFk` int(10) unsigned DEFAULT NULL, `editorFk` int(10) unsigned DEFAULT NULL,
`recycledPlastic` int(11) DEFAULT NULL, `recycledPlastic` int(11) DEFAULT NULL,
`nonRecycledPlastic` int(11) DEFAULT NULL, `nonRecycledPlastic` int(11) DEFAULT NULL,
`minQuantity` int(10) unsigned DEFAULT NULL COMMENT 'Cantidad mínima para una línea de venta',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `item_supplyResponseFk_idx` (`supplyResponseFk`), UNIQUE KEY `item_supplyResponseFk_idx` (`supplyResponseFk`),
KEY `Color` (`inkFk`), KEY `Color` (`inkFk`),

View File

@ -189,5 +189,6 @@
"The sales do not exists": "The sales do not exists", "The sales do not exists": "The sales do not exists",
"Ticket without Route": "Ticket without route", "Ticket without Route": "Ticket without route",
"Booking completed": "Booking complete", "Booking completed": "Booking complete",
"The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation" "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation",
} "You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets"
}

View File

@ -320,5 +320,7 @@
"The response is not a PDF": "La respuesta no es un PDF", "The response is not a PDF": "La respuesta no es un PDF",
"Ticket without Route": "Ticket sin ruta", "Ticket without Route": "Ticket sin ruta",
"Booking completed": "Reserva completada", "Booking completed": "Reserva completada",
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación" "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina"
} }

View File

@ -64,7 +64,7 @@ describe('claim regularizeClaim()', () => {
claimEnds = await importTicket(ticketId, claimId, userId, options); claimEnds = await importTicket(ticketId, claimId, userId, options);
for (claimEnd of claimEnds) for (const claimEnd of claimEnds)
await claimEnd.updateAttributes({claimDestinationFk: trashDestination}, options); await claimEnd.updateAttributes({claimDestinationFk: trashDestination}, options);
let claimBefore = await models.Claim.findById(claimId, null, options); let claimBefore = await models.Claim.findById(claimId, null, options);

View File

@ -17,6 +17,7 @@ Search claim by id or client name: Buscar reclamaciones por identificador o nomb
Claim deleted!: Reclamación eliminada! Claim deleted!: Reclamación eliminada!
claim: reclamación claim: reclamación
Photos: Fotos Photos: Fotos
Development: Trazabilidad
Go to the claim: Ir a la reclamación Go to the claim: Ir a la reclamación
Sale tracking: Líneas preparadas Sale tracking: Líneas preparadas
Ticket tracking: Estados del ticket Ticket tracking: Estados del ticket

View File

@ -131,6 +131,9 @@
"nonRecycledPlastic": { "nonRecycledPlastic": {
"type": "number" "type": "number"
}, },
"minQuantity": {
"type": "number"
},
"packingOut": { "packingOut": {
"type": "number" "type": "number"
}, },
@ -154,6 +157,10 @@
"mysql":{ "mysql":{
"columnName": "doPhoto" "columnName": "doPhoto"
} }
},
"minQuantity": {
"type": "number",
"description": "Min quantity"
} }
}, },
"relations": { "relations": {

View File

@ -33,6 +33,8 @@
rule rule
info="Full name calculates based on tags 1-3. Is not recommended to change it manually"> info="Full name calculates based on tags 1-3. Is not recommended to change it manually">
</vn-textfield> </vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete <vn-autocomplete
url="ItemTypes" url="ItemTypes"
label="Type" label="Type"
@ -50,6 +52,30 @@
</div> </div>
</tpl-item> </tpl-item>
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete
label="Generic"
url="Items/withName"
ng-model="$ctrl.item.genericFk"
vn-name="generic"
show-field="name"
value-field="id"
search-function="$ctrl.itemSearchFunc($search)"
order="id DESC"
tabindex="1">
<tpl-item>
<div>{{::name}}</div>
<div class="text-caption text-secondary">
#{{::id}}
</div>
</tpl-item>
<append>
<vn-icon-button
icon="filter_alt"
vn-click-stop="$ctrl.showFilterDialog($ctrl.item)"
vn-tooltip="Filter...">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
@ -128,30 +154,13 @@
ng-model="$ctrl.item.stemMultiplier" ng-model="$ctrl.item.stemMultiplier"
vn-name="stemMultiplier"> vn-name="stemMultiplier">
</vn-input-number> </vn-input-number>
<vn-autocomplete <vn-input-number
label="Generic" min="1"
url="Items/withName" label="Minimum sales quantity"
ng-model="$ctrl.item.genericFk" ng-model="$ctrl.item.minQuantity"
vn-name="generic" vn-name="minQuantity"
show-field="name" rule>
value-field="id" </vn-input-number>
search-function="$ctrl.itemSearchFunc($search)"
order="id DESC"
tabindex="1">
<tpl-item>
<div>{{::name}}</div>
<div class="text-caption text-secondary">
#{{::id}}
</div>
</tpl-item>
<append>
<vn-icon-button
icon="filter_alt"
vn-click-stop="$ctrl.showFilterDialog($ctrl.item)"
vn-tooltip="Filter...">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-input-number <vn-input-number

View File

@ -16,3 +16,4 @@ This item does need a photo: Este artículo necesita una foto
Do photo: Hacer foto Do photo: Hacer foto
Recycled Plastic: Plástico reciclado Recycled Plastic: Plástico reciclado
Non recycled plastic: Plástico no reciclado Non recycled plastic: Plástico no reciclado
Minimum sales quantity: Cantidad mínima de venta

View File

@ -128,6 +128,9 @@
<vn-label-value label="Non recycled plastic" <vn-label-value label="Non recycled plastic"
value="{{$ctrl.summary.item.nonRecycledPlastic}}"> value="{{$ctrl.summary.item.nonRecycledPlastic}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Minimum sales quantity"
value="{{$ctrl.summary.item.minQuantity}}">
</vn-label-value>
</vn-one> </vn-one>
<vn-one name="tags"> <vn-one name="tags">
<h4 ng-show="$ctrl.isBuyer || $ctrl.isReplenisher"> <h4 ng-show="$ctrl.isBuyer || $ctrl.isReplenisher">

View File

@ -2,3 +2,4 @@ Barcode: Códigos de barras
Other data: Otros datos Other data: Otros datos
Go to the item: Ir al artículo Go to the item: Ir al artículo
WarehouseFk: Calculado sobre el almacén de {{ warehouseName }} WarehouseFk: Calculado sobre el almacén de {{ warehouseName }}
Minimum sales quantity: Cantidad mínima de venta

View File

@ -100,31 +100,32 @@ module.exports = Self => {
)); ));
stmt = new ParameterizedSQL(` stmt = new ParameterizedSQL(`
SELECT SELECT i.id,
i.id, i.name,
i.name, i.subName,
i.subName, i.image,
i.image, i.tag5,
i.tag5, i.value5,
i.value5, i.tag6,
i.tag6, i.value6,
i.value6, i.tag7,
i.tag7, i.value7,
i.value7, i.tag8,
i.tag8, i.value8,
i.value8, i.stars,
i.stars, tci.price,
tci.price, tci.available,
tci.available, w.lastName,
w.lastName AS lastName, w.firstName,
w.firstName, tci.priceKg,
tci.priceKg, ink.hex,
ink.hex i.minQuantity
FROM tmp.ticketCalculateItem tci FROM tmp.ticketCalculateItem tci
JOIN vn.item i ON i.id = tci.itemFk JOIN vn.item i ON i.id = tci.itemFk
JOIN vn.itemType it ON it.id = i.typeFk JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.worker w on w.id = it.workerFk 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 // Apply order by tag
if (orderBy.isTag) { if (orderBy.isTag) {

View File

@ -8,12 +8,12 @@
<div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div> <div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div>
</div> </div>
<img <img
ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}" ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
on-error-src/> on-error-src/>
</div> </div>
<div class="description"> <div class="description">
<h3 class="link" <h3 class="link"
ng-click="itemDescriptor.show($event, item.id)"> ng-click="itemDescriptor.show($event, item.id)">
{{::item.name}} {{::item.name}}
</h3> </h3>
@ -37,13 +37,28 @@
value="{{::item.value7}}"> value="{{::item.value7}}">
</vn-label-value> </vn-label-value>
</div> </div>
<vn-rating ng-if="::item.stars" <vn-horizontal>
ng-model="::item.stars"> <vn-one>
</vn-rating> <vn-rating ng-if="::item.stars"
ng-model="::item.stars"/>
</vn-one>
<vn-horizontal
class="text-right text-caption alert vn-mr-xs"
ng-if="::item.minQuantity">
<vn-one>
<vn-icon
icon="production_quantity_limits"
translate-attr="{title: 'Minimal quantity'}"
class="text-subtitle1">
</vn-icon>
</vn-one>
{{::item.minQuantity}}
</vn-horizontal>
</vn-horizontal>
<div class="footer"> <div class="footer">
<div class="price"> <div class="price">
<vn-one> <vn-one>
<span>{{::item.available}}</span> <span>{{::item.available}}</span>
<span translate>to</span> <span translate>to</span>
<span>{{::item.price | currency:'EUR':2}}</span> <span>{{::item.price | currency:'EUR':2}}</span>
</vn-one> </vn-one>
@ -54,7 +69,7 @@
</vn-icon-button> </vn-icon-button>
</div> </div>
<div class="priceKg" ng-show="::item.priceKg"> <div class="priceKg" ng-show="::item.priceKg">
<span>Precio por kilo {{::item.priceKg | currency: 'EUR'}}</span> <span>Precio por kilo {{::item.priceKg | currency: 'EUR'}}</span>
</div> </div>
</div> </div>
</div> </div>
@ -69,4 +84,4 @@
<vn-item-descriptor-popover <vn-item-descriptor-popover
vn-id="item-descriptor" vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk"> warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover> </vn-item-descriptor-popover>

View File

@ -1 +1,2 @@
Order created: Orden creada Order created: Orden creada
Minimal quantity: Cantidad mínima

View File

@ -44,4 +44,7 @@ vn-order-catalog {
height: 30px; height: 30px;
position: relative; position: relative;
} }
} .alert {
color: $color-alert;
}
}

View File

@ -55,7 +55,7 @@ module.exports = Self => {
const refoundZoneId = refundAgencyMode.zones()[0].id; const refoundZoneId = refundAgencyMode.zones()[0].id;
if (salesIds) { if (salesIds.length) {
const salesFilter = { const salesFilter = {
where: {id: {inq: salesIds}}, where: {id: {inq: salesIds}},
include: { include: {
@ -91,16 +91,14 @@ module.exports = Self => {
await models.SaleComponent.create(components, myOptions); await models.SaleComponent.create(components, myOptions);
} }
} }
if (!refundTicket) { if (!refundTicket) {
const servicesFilter = { const servicesFilter = {
where: {id: {inq: servicesIds}} where: {id: {inq: servicesIds}}
}; };
const services = await models.TicketService.find(servicesFilter, myOptions); 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 now = Date.vnNew();
const [firstTicketId] = ticketsIds;
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions); refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions);
@ -114,8 +112,8 @@ module.exports = Self => {
for (const service of services) { for (const service of services) {
await models.TicketService.create({ await models.TicketService.create({
description: service.description, description: service.description,
quantity: service.quantity, quantity: - service.quantity,
price: - service.price, price: service.price,
taxClassFk: service.taxClassFk, taxClassFk: service.taxClassFk,
ticketFk: refundTicket.id, ticketFk: refundTicket.id,
ticketServiceTypeFk: service.ticketServiceTypeFk, ticketServiceTypeFk: service.ticketServiceTypeFk,

View File

@ -1,21 +1,9 @@
/* eslint max-len: ["error", { "code": 150 }]*/
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('sale updateQuantity()', () => { 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 = { const ctx = {
req: { req: {
accessToken: {userId: 9}, 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() => { it('should throw an error if the quantity is greater than it should be', async() => {
const ctx = { 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({}); const tx = await models.Sale.beginTransaction({});
let error; let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, 17, 99, options); await models.Sale.updateQuantity(ctx, 17, 31, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } 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() => { 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 saleId = 17;
const buyerId = 35; const buyerId = 35;
const ctx = { 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 { try {
const options = {transaction: tx}; const options = {transaction: tx};
@ -87,6 +92,8 @@ describe('sale updateQuantity()', () => {
}); });
it('should update the quantity of a given sale current line', async() => { 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 tx = await models.Sale.beginTransaction({});
const saleId = 25; const saleId = 25;
const newQuantity = 4; const newQuantity = 4;
@ -119,6 +126,8 @@ describe('sale updateQuantity()', () => {
__: () => {} __: () => {}
} }
}; };
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
const saleId = 17; const saleId = 17;
const newQuantity = -10; const newQuantity = -10;
@ -140,6 +149,8 @@ describe('sale updateQuantity()', () => {
}); });
it('should update a negative quantity when is a ticket refund', async() => { 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 tx = await models.Sale.beginTransaction({});
const saleId = 13; const saleId = 13;
const newQuantity = -10; const newQuantity = -10;
@ -159,4 +170,70 @@ describe('sale updateQuantity()', () => {
throw e; 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;
}
});
}); });

View File

@ -1,4 +1,3 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('updateQuantity', { Self.remoteMethodCtx('updateQuantity', {
@ -64,17 +63,6 @@ module.exports = Self => {
const sale = await models.Sale.findById(id, filter, myOptions); 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 oldQuantity = sale.quantity;
const result = await sale.updateAttributes({quantity: newQuantity}, myOptions); const result = await sale.updateAttributes({quantity: newQuantity}, myOptions);

View File

@ -63,17 +63,6 @@ module.exports = Self => {
} }
}, myOptions); }, 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({ const newSale = await models.Sale.create({
ticketFk: id, ticketFk: id,
itemFk: item.id, itemFk: item.id,

View File

@ -1,3 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
const LoopBackContext = require('loopback-context');
module.exports = Self => { module.exports = Self => {
require('../methods/sale/getClaimableFromTicket')(Self); require('../methods/sale/getClaimableFromTicket')(Self);
require('../methods/sale/reserve')(Self); require('../methods/sale/reserve')(Self);
@ -13,4 +16,77 @@ module.exports = Self => {
Self.validatesPresenceOf('concept', { Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank` 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');
});
}; };

View File

@ -16,8 +16,8 @@ class Controller extends SearchPanel {
this.$http.get('ItemPackingTypes', {filter}).then(res => { this.$http.get('ItemPackingTypes', {filter}).then(res => {
for (let ipt of res.data) { for (let ipt of res.data) {
itemPackingTypes.push({ itemPackingTypes.push({
code: ipt.code, description: this.$t(ipt.description),
description: this.$t(ipt.description) code: ipt.code
}); });
} }
this.itemPackingTypes = itemPackingTypes; this.itemPackingTypes = itemPackingTypes;

View File

@ -163,26 +163,16 @@ export default class Controller extends Section {
return {'futureId': value}; return {'futureId': value};
case 'liters': case 'liters':
return {'liters': value}; return {'liters': value};
case 'lines':
return {'lines': value};
case 'futureLiters': case 'futureLiters':
return {'futureLiters': value}; return {'futureLiters': value};
case 'lines':
return {'lines': value};
case 'futureLines': case 'futureLines':
return {'futureLines': value}; return {'futureLines': value};
case 'ipt': case 'ipt':
return {or: return {'ipt': {like: `%${value}%`}};
[
{'ipt': {like: `%${value}%`}},
{'ipt': null}
]
};
case 'futureIpt': case 'futureIpt':
return {or: return {'futureIpt': {like: `%${value}%`}};
[
{'futureIpt': {like: `%${value}%`}},
{'futureIpt': null}
]
};
case 'totalWithVat': case 'totalWithVat':
return {'totalWithVat': value}; return {'totalWithVat': value};
case 'futureTotalWithVat': case 'futureTotalWithVat':

View File

@ -18,3 +18,4 @@ Multiple invoice: Factura múltiple
Make invoice...: Crear factura... Make invoice...: Crear factura...
Invoice selected tickets: Facturar tickets seleccionados Invoice selected tickets: Facturar tickets seleccionados
Are you sure to invoice tickets: ¿Seguro que quieres facturar {{ticketsAmount}} tickets? Are you sure to invoice tickets: ¿Seguro que quieres facturar {{ticketsAmount}} tickets?
Rounding: Redondeo

View File

@ -311,7 +311,7 @@
clear-disabled="true" clear-disabled="true"
suffix="%"> suffix="%">
</vn-input-number> </vn-input-number>
<vn-vertical ng-if="$ctrl.usesMana && $ctrl.currentWorkerMana != 0"> <vn-vertical ng-if="$ctrl.usesMana">
<vn-radio <vn-radio
label="Promotion mana" label="Promotion mana"
val="mana" val="mana"

View File

@ -97,14 +97,6 @@ class Controller extends Section {
}); });
}); });
this.getUsesMana(); this.getUsesMana();
this.getCurrentWorkerMana();
}
getCurrentWorkerMana() {
this.$http.get(`WorkerManas/getCurrentWorkerMana`)
.then(res => {
this.currentWorkerMana = res.data;
});
} }
getUsesMana() { getUsesMana() {

View File

@ -120,12 +120,10 @@ describe('Ticket', () => {
const expectedAmount = 250; const expectedAmount = 250;
$httpBackend.expect('GET', 'Tickets/1/getSalesPersonMana').respond(200, expectedAmount); $httpBackend.expect('GET', 'Tickets/1/getSalesPersonMana').respond(200, expectedAmount);
$httpBackend.expect('GET', 'Sales/usesMana').respond(200); $httpBackend.expect('GET', 'Sales/usesMana').respond(200);
$httpBackend.expect('GET', 'WorkerManas/getCurrentWorkerMana').respond(200, expectedAmount);
controller.getMana(); controller.getMana();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.edit.mana).toEqual(expectedAmount); expect(controller.edit.mana).toEqual(expectedAmount);
expect(controller.currentWorkerMana).toEqual(expectedAmount);
}); });
}); });

View File

@ -29,7 +29,7 @@
disabled="watcher.dataChanged() || !$ctrl.checkeds.length" disabled="watcher.dataChanged() || !$ctrl.checkeds.length"
label="Pay" label="Pay"
ng-click="$ctrl.createRefund()" ng-click="$ctrl.createRefund()"
vn-acl="invoicing, claimManager, salesAssistant" vn-acl="invoicing, claimManager, salesAssistant, buyer"
vn-acl-action="remove"> vn-acl-action="remove">
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
@ -37,7 +37,9 @@
<vn-check <vn-check
tabindex="1" tabindex="1"
on-change="$ctrl.addChecked(service.id)" on-change="$ctrl.addChecked(service.id)"
disabled="!service.id"> disabled="!service.id"
vn-acl="invoicing, claimManager, salesAssistant, buyer"
vn-acl-action="remove">
</vn-check> </vn-check>
<vn-autocomplete vn-two vn-focus <vn-autocomplete vn-two vn-focus
data="ticketServiceTypes" data="ticketServiceTypes"
@ -68,7 +70,8 @@
vn-one vn-one
label="Price" label="Price"
ng-model="service.price" ng-model="service.price"
step="0.01"> step="0.01"
min="0">
</vn-input-number> </vn-input-number>
<vn-auto> <vn-auto>
<vn-icon-button <vn-icon-button

View File

@ -1,26 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('getCurrentWorkerMana', {
description: 'Returns the mana of the logged worker',
accessType: 'READ',
accepts: [],
returns: {
type: 'number',
root: true
},
http: {
path: `/getCurrentWorkerMana`,
verb: 'GET'
}
});
Self.getCurrentWorkerMana = async ctx => {
let userId = ctx.req.accessToken.userId;
let workerMana = await Self.app.models.WorkerMana.findOne({
where: {workerFk: userId},
fields: 'amount'
});
return workerMana ? workerMana.amount : 0;
};
};

View File

@ -1,15 +0,0 @@
const app = require('vn-loopback/server/server');
describe('workerMana getCurrentWorkerMana()', () => {
it('should get the mana of the logged worker', async() => {
let mana = await app.models.WorkerMana.getCurrentWorkerMana({req: {accessToken: {userId: 18}}});
expect(mana).toEqual(124);
});
it('should return 0 if the user doesnt uses mana', async() => {
let mana = await app.models.WorkerMana.getCurrentWorkerMana({req: {accessToken: {userId: 9}}});
expect(mana).toEqual(0);
});
});

View File

@ -66,46 +66,36 @@ module.exports = Self => {
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate');
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeBusinessCalculate'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeBusinessCalculate');
const destroyAllWhere = {
timed: {between: [started, ended]},
isSendMail: true
};
const updateAllWhere = {
year: args.year,
week: args.week
};
const tmpUserSQL = `
CREATE OR REPLACE TEMPORARY TABLE tmp.user
SELECT id as userFk
FROM vn.worker`;
let tmpUser = new ParameterizedSQL(tmpUserSQL);
if (args.workerId) { if (args.workerId) {
await models.WorkerTimeControl.destroyAll({ destroyAllWhere.userFk = args.workerId;
userFk: args.workerId, updateAllWhere.workerFk = args.workerId;
timed: {between: [started, ended]}, tmpUser = new ParameterizedSQL(tmpUserSQL + ' WHERE id = ?', [args.workerId]);
isSendMail: true
}, myOptions);
const where = {
workerFk: args.workerId,
year: args.year,
week: args.week
};
await models.WorkerTimeControlMail.updateAll(where, {
updated: Date.vnNew(), state: 'SENDED'
}, myOptions);
stmt = new ParameterizedSQL('DROP TEMPORARY TABLE IF EXISTS tmp.`user`');
stmts.push(stmt);
stmt = new ParameterizedSQL('CREATE TEMPORARY TABLE tmp.`user` SELECT id userFk FROM account.user WHERE id = ?', [args.workerId]);
stmts.push(stmt);
} else {
await models.WorkerTimeControl.destroyAll({
timed: {between: [started, ended]},
isSendMail: true
}, myOptions);
const where = {
year: args.year,
week: args.week
};
await models.WorkerTimeControlMail.updateAll(where, {
updated: Date.vnNew(), state: 'SENDED'
}, myOptions);
stmt = new ParameterizedSQL('DROP TEMPORARY TABLE IF EXISTS tmp.`user`');
stmts.push(stmt);
stmt = new ParameterizedSQL('CREATE TEMPORARY TABLE IF NOT EXISTS tmp.`user` SELECT id as userFk FROM vn.worker w JOIN account.`user` u ON u.id = w.id WHERE id IS NOT NULL');
stmts.push(stmt);
} }
await models.WorkerTimeControl.destroyAll(destroyAllWhere, myOptions);
await models.WorkerTimeControlMail.updateAll(updateAllWhere, {
updated: Date.vnNew(),
state: 'SENDED'
}, myOptions);
stmts.push(tmpUser);
stmt = new ParameterizedSQL( stmt = new ParameterizedSQL(
`CALL vn.timeControl_calculate(?, ?) `CALL vn.timeControl_calculate(?, ?)
`, [started, ended]); `, [started, ended]);

View File

@ -46,7 +46,7 @@ module.exports = Self => {
SELECT DISTINCT w.id, w.code, u.name, u.nickname, u.active, b.departmentFk SELECT DISTINCT w.id, w.code, u.name, u.nickname, u.active, b.departmentFk
FROM worker w FROM worker w
JOIN account.user u ON u.id = w.id JOIN account.user u ON u.id = w.id
JOIN business b ON b.workerFk = w.id LEFT JOIN business b ON b.workerFk = w.id
) w`); ) w`);
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));

View File

@ -1,3 +0,0 @@
module.exports = Self => {
require('../methods/worker-mana/getCurrentWorkerMana')(Self);
};

View File

@ -117,7 +117,7 @@
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}</td> <td class="number">{{service.price | currency('EUR', $i18n.locale)}}</td>
<td class="centered" width="5%"></td> <td class="centered" width="5%"></td>
<td class="centered">{{service.taxDescription}}</td> <td class="centered">{{service.taxDescription}}</td>
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}</td> <td class="number">{{service.total | currency('EUR', $i18n.locale)}}</td>
</tr> </tr>
</tbody> </tbody>
<tfoot> <tfoot>

View File

@ -1,8 +1,9 @@
SELECT SELECT
tc.code taxDescription, tc.code taxDescription,
ts.description, ts.description,
ts.quantity, ts.quantity,
ts.price ts.price,
ts.quantity * ts.price total
FROM ticketService ts FROM ticketService ts
JOIN taxClass tc ON tc.id = ts.taxClassFk JOIN taxClass tc ON tc.id = ts.taxClassFk
WHERE ts.ticketFk = ? WHERE ts.ticketFk = ?