diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dbc270b9..acb5d7457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2338.01] - 2023-09-21 ### Added - +- (Ticket -> Servicios) Se pueden abonar servicios ### Changed - (Trabajadores -> Calendario) Icono de check arreglado cuando pulsas un tipo de dia diff --git a/db/changes/233601/00-firstScript.sql b/db/changes/233601/00-firstScript.sql new file mode 100644 index 000000000..47b96b3bc --- /dev/null +++ b/db/changes/233601/00-firstScript.sql @@ -0,0 +1 @@ +ALTER TABLE `vn`.`province` ADD CONSTRAINT `countryName_UN` UNIQUE KEY (`countryFk`,`name`); diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index fe3633723..1b18eb866 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -671,8 +671,8 @@ export default { firstAddServiceTypeButton: 'vn-ticket-service vn-icon-button[vn-tooltip="New service type"]', firstServiceType: 'vn-ticket-service vn-autocomplete[ng-model="service.ticketServiceTypeFk"]', firstQuantity: 'vn-ticket-service vn-input-number[ng-model="service.quantity"]', - firstPrice: 'vn-ticket-service vn-horizontal:nth-child(1) vn-input-number[ng-model="service.price"]', - fistDeleteServiceButton: 'vn-ticket-service form vn-horizontal:nth-child(1) vn-icon-button[icon="delete"]', + firstPrice: 'vn-ticket-service vn-horizontal:nth-child(2) vn-input-number[ng-model="service.price"]', + fistDeleteServiceButton: 'vn-ticket-service form vn-horizontal:nth-child(2) vn-icon-button[icon="delete"]', newServiceTypeName: '.vn-dialog.shown vn-textfield[ng-model="newServiceType.name"]', serviceLine: 'vn-ticket-service > form > vn-card > vn-one:nth-child(2) > vn-horizontal', saveServiceButton: 'button[type=submit]', diff --git a/e2e/paths/09-invoice-in/03_basic_data.spec.js b/e2e/paths/09-invoice-in/03_basic_data.spec.js index 778b5949c..50fe18830 100644 --- a/e2e/paths/09-invoice-in/03_basic_data.spec.js +++ b/e2e/paths/09-invoice-in/03_basic_data.spec.js @@ -82,32 +82,24 @@ describe('InvoiceIn basic data path', () => { await page.waitToClick(selectors.invoiceInBasicData.confirm); let message = await page.waitForSnackbar(); - expect(message.text).toContain('The company can\'t be empty'); - await page.clearInput(selectors.invoiceInBasicData.companyId); await page.autocompleteSearch(selectors.invoiceInBasicData.companyId, 'VNL'); await page.waitToClick(selectors.invoiceInBasicData.confirm); message = await page.waitForSnackbar(); - expect(message.text).toContain('The warehouse can\'t be empty'); - await page.clearInput(selectors.invoiceInBasicData.warehouseId); await page.autocompleteSearch(selectors.invoiceInBasicData.warehouseId, 'Warehouse One'); await page.waitToClick(selectors.invoiceInBasicData.confirm); message = await page.waitForSnackbar(); - expect(message.text).toContain('The DMS Type can\'t be empty'); - await page.clearInput(selectors.invoiceInBasicData.dmsTypeId); await page.autocompleteSearch(selectors.invoiceInBasicData.dmsTypeId, 'Ticket'); await page.waitToClick(selectors.invoiceInBasicData.confirm); message = await page.waitForSnackbar(); - expect(message.text).toContain('The description can\'t be empty'); - await page.waitToClick(selectors.invoiceInBasicData.description); await page.write(selectors.invoiceInBasicData.description, 'Dms without edition.'); diff --git a/front/core/lib/validator.js b/front/core/lib/validator.js index b0b8f7389..51134f30a 100644 --- a/front/core/lib/validator.js +++ b/front/core/lib/validator.js @@ -5,6 +5,10 @@ export const validators = { if (validator.isEmpty(value ? String(value) : '')) throw new Error(_($translate, `Value can't be empty`)); }, + negative: $translate => { + if (validator < 0) + throw new Error(_($translate, `Negative numbers are not allowed. Please enter a valid number.`)); + }, absence: ($translate, value) => { if (!validator.isEmpty(value)) throw new Error(_($translate, `Value should be empty`)); @@ -104,9 +108,8 @@ export function checkNull($translate, value, conf) { export function _($translate, text, params = []) { text = $translate.instant(text); - for (let i = 0; i < params.length; i++) { + for (let i = 0; i < params.length; i++) text = text.replace('%s', params[i]); - } return text; } diff --git a/front/core/locale/es.yml b/front/core/locale/es.yml index e9c2311b4..96c34d98c 100644 --- a/front/core/locale/es.yml +++ b/front/core/locale/es.yml @@ -16,6 +16,7 @@ Value can't be empty: El valor no puede estar vacío Value should be empty: El valor debe estar vacío Value should be integer: El valor debe ser entero Value should be a number: El valor debe ser numérico +Negative numbers are not allowed. Please enter a valid number: No se permiten números negativos. Por favor, ingrese un número válido Invalid value: Valor incorrecto Value can't be blank: El valor no puede estar en blanco Value can't be null: El valor no puede ser nulo diff --git a/loopback/locale/es.json b/loopback/locale/es.json index ccd63e0ff..73a3920d2 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -307,7 +307,7 @@ "Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}", "You cannot assign an alias that you are not assigned to": "No puede asignar un alias que no tenga asignado", "This ticket cannot be left empty.": "Este ticket no se puede dejar vacío. %s", - "The company has not informed the supplier account for bank transfers": "La empresa no tiene informado la cuenta de proveedor para transferencias bancarias", + "The company has not informed the supplier account for bank transfers": "La empresa no tiene informado la cuenta de proveedor para transferencias bancarias", "You cannot assign/remove an alias that you are not assigned to": "No puede asignar/eliminar un alias que no tenga asignado", "This invoice has a linked vehicle.": "Esta factura tiene un vehiculo vinculado", "You don't have enough privileges.": "No tienes suficientes permisos.", diff --git a/modules/supplier/back/models/supplier-account.js b/modules/supplier/back/models/supplier-account.js index 691e72580..7c68e2c98 100644 --- a/modules/supplier/back/models/supplier-account.js +++ b/modules/supplier/back/models/supplier-account.js @@ -13,7 +13,7 @@ module.exports = Self => { const bankEntity = await Self.app.models.BankEntity.findById(this.bankEntityFk); const filter = { fields: ['code'], - where: {id: bankEntity.countryFk} + where: {id: bankEntity?.countryFk} }; const country = await Self.app.models.Country.findOne(filter); diff --git a/modules/ticket/back/methods/sale/refund.js b/modules/ticket/back/methods/sale/refund.js index 12f240ae2..3c41aab1e 100644 --- a/modules/ticket/back/methods/sale/refund.js +++ b/modules/ticket/back/methods/sale/refund.js @@ -5,8 +5,7 @@ module.exports = Self => { accepts: [ { arg: 'salesIds', - type: ['number'], - required: true + type: ['number'] }, { arg: 'servicesIds', @@ -41,6 +40,7 @@ module.exports = Self => { myOptions.transaction = tx; } + let refundTicket = null; try { const refundAgencyMode = await models.AgencyMode.findOne({ include: { @@ -55,42 +55,55 @@ module.exports = Self => { const refoundZoneId = refundAgencyMode.zones()[0].id; - const salesFilter = { - where: {id: {inq: salesIds}}, - include: { - relation: 'components', - scope: { - fields: ['saleFk', 'componentFk', 'value'] + if (salesIds) { + const salesFilter = { + where: {id: {inq: salesIds}}, + include: { + relation: 'components', + scope: { + fields: ['saleFk', 'componentFk', 'value'] + } } + }; + const sales = await models.Sale.find(salesFilter, myOptions); + const ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))]; + + const now = Date.vnNew(); + const [firstTicketId] = ticketsIds; + + // eslint-disable-next-line max-len + refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions); + + for (const sale of sales) { + const createdSale = await models.Sale.create({ + ticketFk: refundTicket.id, + itemFk: sale.itemFk, + quantity: - sale.quantity, + concept: sale.concept, + price: sale.price, + discount: sale.discount, + }, myOptions); + + const components = sale.components(); + for (const component of components) + component.saleFk = createdSale.id; + + await models.SaleComponent.create(components, myOptions); } - }; - const sales = await models.Sale.find(salesFilter, myOptions); - const ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))]; - - let [firstTicketId] = ticketsIds; - if (!firstTicketId) { - [ticketServices] = await models.TicketService.find({where: {id: {inq: servicesIds}}}, myOptions); - firstTicketId = ticketServices.ticketFk; } - const now = Date.vnNew(); - const refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, 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))]; - for (const sale of sales) { - const createdSale = await models.Sale.create({ - ticketFk: refundTicket.id, - itemFk: sale.itemFk, - quantity: - sale.quantity, - concept: sale.concept, - price: sale.price, - discount: sale.discount, - }, myOptions); + const now = Date.vnNew(); + const [firstTicketId] = ticketsIds; - const components = sale.components(); - for (const component of components) - component.saleFk = createdSale.id; - - await models.SaleComponent.create(components, myOptions); + // eslint-disable-next-line max-len + refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions); } if (servicesIds && servicesIds.length > 0) { @@ -101,8 +114,8 @@ module.exports = Self => { for (const service of services) { await models.TicketService.create({ description: service.description, - quantity: - service.quantity, - price: service.price, + quantity: service.quantity, + price: - service.price, taxClassFk: service.taxClassFk, ticketFk: refundTicket.id, ticketServiceTypeFk: service.ticketServiceTypeFk, diff --git a/modules/ticket/front/services/index.html b/modules/ticket/front/services/index.html index bb5505ce6..bc288a8a2 100644 --- a/modules/ticket/front/services/index.html +++ b/modules/ticket/front/services/index.html @@ -1,4 +1,4 @@ -
- + + + + + + + + rule="TicketService" + min="0"> - + - - - - \ No newline at end of file + diff --git a/modules/ticket/front/services/index.js b/modules/ticket/front/services/index.js index 4866fd8bb..d8c209ea4 100644 --- a/modules/ticket/front/services/index.js +++ b/modules/ticket/front/services/index.js @@ -6,6 +6,8 @@ class Controller extends Section { $onInit() { this.services = []; this.getDefaultTaxClass(); + this.isDataSaved = false; + this.checkeds = []; } getDefaultTaxClass() { @@ -49,6 +51,26 @@ class Controller extends Section { .then(() => this.$.model.refresh()) .then(() => this.$.watcher.notifySaved()); } + + createRefund() { + if (!this.checkeds.length) return; + + const params = {servicesIds: this.checkeds, withWarehouse: false}; + const query = 'Sales/refund'; + this.$http.post(query, params).then(res => { + const refundTicket = res.data; + this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { + ticketId: refundTicket.id + })); + this.$state.go('ticket.card.sale', {id: refundTicket.id}); + }); + } + + addChecked(id) { + if (this.checkeds.includes(id)) + return this.checkeds = this.checkeds.filter(check => check != id); + this.checkeds.push(id); + } } ngModule.vnComponent('vnTicketService', { diff --git a/modules/ticket/front/services/index.spec.js b/modules/ticket/front/services/index.spec.js index affe8a6e7..9293d4c65 100644 --- a/modules/ticket/front/services/index.spec.js +++ b/modules/ticket/front/services/index.spec.js @@ -19,12 +19,11 @@ describe('Ticket component vnTicketService', () => { describe('getDefaultTaxClass', () => { it('should set the default tax class in the controller', () => { - $httpBackend.whenRoute('GET', `TaxClasses/findOne`) - .respond({ - id: 4000, - name: 'Whatever', - code: 'GG' - }); + $httpBackend.whenRoute('GET', `TaxClasses/findOne`).respond({ + id: 4000, + name: 'Whatever', + code: 'GG', + }); controller.getDefaultTaxClass(); $httpBackend.flush(); @@ -49,15 +48,15 @@ describe('Ticket component vnTicketService', () => { it('should set the description of the selected service upon service type creation', () => { const service = { id: 1, - quantity: 10 + quantity: 10, }; $scope.newServiceType = { - name: 'Totally new stuff' + name: 'Totally new stuff', }; $httpBackend.when('POST', 'TicketServiceTypes').respond({ id: 4001, - name: 'Totally new stuff' + name: 'Totally new stuff', }); controller.onNewServiceTypeAccept(service); $httpBackend.flush(); @@ -65,4 +64,20 @@ describe('Ticket component vnTicketService', () => { expect(service.ticketServiceTypeFk).toEqual(4001); }); }); + + describe('addChecked', () => { + it('should add an item to the checkeds array', () => { + controller.checkeds = []; + controller.addChecked(1); + + expect(controller.checkeds).toEqual([1]); + }); + + it('should remove an item if it is already in the checkeds array', () => { + controller.checkeds = [1, 2, 3]; + controller.addChecked(2); + + expect(controller.checkeds).toEqual([1, 3]); + }); + }); }); diff --git a/modules/ticket/front/services/locale/es.yml b/modules/ticket/front/services/locale/es.yml index 08788fc09..08e6c968c 100644 --- a/modules/ticket/front/services/locale/es.yml +++ b/modules/ticket/front/services/locale/es.yml @@ -2,4 +2,6 @@ Service: Servicios Tax class: Tipo IVA Add service: Añadir servicio Remove service: Quitar servicio -New service type: Nuevo tipo de servicio \ No newline at end of file +New service type: Nuevo tipo de servicio +Pay: Abonar +To create services with negative amounts mark the service on the source ticket and press the pay button.: Para crear sevicios con cantidades negativas marcar servicio en el ticket origen y apretar el boton abonar. \ No newline at end of file