diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 12bd6e640..dd80f0021 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -1036,6 +1036,21 @@ export default { entriesQuicklink: 'vn-entry-descriptor vn-quick-link[icon="icon-entry"] > a' }, entryBuys: { + anyBuyLine: 'vn-entry-buy-index tr.dark-row', + allBuyCheckbox: 'vn-entry-buy-index thead vn-check', + firstBuyCheckbox: 'vn-entry-buy-index tbody:nth-child(2) vn-check', + deleteBuysButton: 'vn-entry-buy-index vn-button[icon="delete"]', + addBuyButton: 'vn-entry-buy-index vn-icon[icon="add_circle"]', + secondBuyPackingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price3"]', + secondBuyGroupingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price2"]', + secondBuyPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.buyingValue"]', + secondBuyGrouping: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.grouping"]', + secondBuyPacking: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.packing"]', + secondBuyWeight: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.weight"]', + secondBuyStickers: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.stickers"]', + secondBuyPackage: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.packageFk"]', + secondBuyQuantity: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.quantity"]', + secondBuyItem: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.itemFk"]', importButton: 'vn-entry-buy-index vn-icon[icon="publish"]', ref: 'vn-entry-buy-import vn-textfield[ng-model="$ctrl.import.ref"]', observation: 'vn-entry-buy-import vn-textarea[ng-model="$ctrl.import.observation"]', diff --git a/e2e/paths/12-entry/07_buys.spec.js b/e2e/paths/12-entry/07_buys.spec.js new file mode 100644 index 000000000..e5617b8bd --- /dev/null +++ b/e2e/paths/12-entry/07_buys.spec.js @@ -0,0 +1,187 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Entry import, create and edit buys path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'entry'); + await page.accessToSearchResult('1'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should count the summary buys and find there only one at this point', async() => { + const buysCount = await page.countElement(selectors.entrySummary.anyBuyLine); + + expect(buysCount).toEqual(1); + }); + + it('should navigate to the buy section and then click the import button opening the import form', async() => { + await page.accessToSection('entry.card.buy.index'); + await page.waitToClick(selectors.entryBuys.importButton); + await page.waitForState('entry.card.buy.import'); + }); + + it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => { + await page.write(selectors.entryBuys.ref, 'a reference'); + await page.write(selectors.entryBuys.observation, 'an observation'); + + let currentDir = process.cwd(); + let filePath = `${currentDir}/e2e/assets/07_import_buys.json`; + + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.waitToClick(selectors.entryBuys.file) + ]); + await fileChooser.accept([filePath]); + + await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm'); + await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m'); + await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m'); + await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Container ammo box 1m'); + + await page.waitToClick(selectors.entryBuys.importBuysButton); + + const message = await page.waitForSnackbar(); + const state = await page.getState(); + + expect(message.text).toContain('Data saved!'); + expect(state).toBe('entry.card.buy.index'); + }); + + it('should count the buys to find 4 buys have been added', async() => { + await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 5); + }); + + it('should delete the four buys that were just added', async() => { + await page.waitToClick(selectors.entryBuys.allBuyCheckbox); + await page.waitToClick(selectors.entryBuys.firstBuyCheckbox); + await page.waitToClick(selectors.entryBuys.deleteBuysButton); + await page.waitToClick(selectors.globalItems.acceptButton); + await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 1); + }); + + it('should add a new buy', async() => { + await page.waitToClick(selectors.entryBuys.addBuyButton); + await page.write(selectors.entryBuys.secondBuyPackingPrice, '999'); + await page.write(selectors.entryBuys.secondBuyGroupingPrice, '999'); + await page.write(selectors.entryBuys.secondBuyPrice, '999'); + await page.write(selectors.entryBuys.secondBuyGrouping, '999'); + await page.write(selectors.entryBuys.secondBuyPacking, '999'); + await page.write(selectors.entryBuys.secondBuyWeight, '999'); + await page.write(selectors.entryBuys.secondBuyStickers, '999'); + await page.autocompleteSearch(selectors.entryBuys.secondBuyPackage, '1'); + await page.write(selectors.entryBuys.secondBuyQuantity, '999'); + await page.autocompleteSearch(selectors.entryBuys.secondBuyItem, '1'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + + await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 2); + }); + + it('should edit the newest buy', async() => { + await page.clearInput(selectors.entryBuys.secondBuyPackingPrice); + await page.waitForTextInField(selectors.entryBuys.secondBuyPackingPrice, ''); + await page.write(selectors.entryBuys.secondBuyPackingPrice, '100'); + await page.waitForSnackbar(); + + await page.clearInput(selectors.entryBuys.secondBuyGroupingPrice); + await page.waitForTextInField(selectors.entryBuys.secondBuyGroupingPrice, ''); + await page.write(selectors.entryBuys.secondBuyGroupingPrice, '200'); + await page.waitForSnackbar(); + + await page.clearInput(selectors.entryBuys.secondBuyPrice); + await page.waitForTextInField(selectors.entryBuys.secondBuyPrice, ''); + await page.write(selectors.entryBuys.secondBuyPrice, '300'); + await page.waitForSnackbar(); + + await page.clearInput(selectors.entryBuys.secondBuyGrouping); + await page.waitForTextInField(selectors.entryBuys.secondBuyGrouping, ''); + await page.write(selectors.entryBuys.secondBuyGrouping, '400'); + await page.waitForSnackbar(); + + await page.clearInput(selectors.entryBuys.secondBuyPacking); + await page.waitForTextInField(selectors.entryBuys.secondBuyPacking, ''); + await page.write(selectors.entryBuys.secondBuyPacking, '500'); + await page.waitForSnackbar(); + + await page.clearInput(selectors.entryBuys.secondBuyWeight); + await page.waitForTextInField(selectors.entryBuys.secondBuyWeight, ''); + await page.write(selectors.entryBuys.secondBuyWeight, '600'); + await page.waitForSnackbar(); + + await page.clearInput(selectors.entryBuys.secondBuyStickers); + await page.waitForTextInField(selectors.entryBuys.secondBuyStickers, ''); + await page.write(selectors.entryBuys.secondBuyStickers, '700'); + await page.waitForSnackbar(); + + await page.autocompleteSearch(selectors.entryBuys.secondBuyPackage, '94'); + await page.waitForSnackbar(); + + await page.clearInput(selectors.entryBuys.secondBuyQuantity); + await page.waitForTextInField(selectors.entryBuys.secondBuyQuantity, ''); + await page.write(selectors.entryBuys.secondBuyQuantity, '800'); + }); + + it('should reload the section and check the packing price is as expected', async() => { + await page.reloadSection('entry.card.buy.index'); + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyPackingPrice, 'value'); + + expect(result).toEqual('100'); + }); + + it('should reload the section and check the grouping price is as expected', async() => { + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyGroupingPrice, 'value'); + + expect(result).toEqual('200'); + }); + + it('should reload the section and check the price is as expected', async() => { + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyPrice, 'value'); + + expect(result).toEqual('300'); + }); + + it('should reload the section and check the grouping is as expected', async() => { + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyGrouping, 'value'); + + expect(result).toEqual('400'); + }); + + it('should reload the section and check the packing is as expected', async() => { + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyPacking, 'value'); + + expect(result).toEqual('500'); + }); + + it('should reload the section and check the weight is as expected', async() => { + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyWeight, 'value'); + + expect(result).toEqual('600'); + }); + + it('should reload the section and check the stickers are as expected', async() => { + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyStickers, 'value'); + + expect(result).toEqual('700'); + }); + + it('should reload the section and check the package is as expected', async() => { + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyPackage, 'value'); + + expect(result).toEqual('94'); + }); + + it('should reload the section and check the quantity is as expected', async() => { + const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyQuantity, 'value'); + + expect(result).toEqual('800'); + }); +}); diff --git a/e2e/paths/12-entry/07_import_buys.spec.js b/e2e/paths/12-entry/07_import_buys.spec.js deleted file mode 100644 index 02db0ded5..000000000 --- a/e2e/paths/12-entry/07_import_buys.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import selectors from '../../helpers/selectors.js'; -import getBrowser from '../../helpers/puppeteer'; - -describe('Entry import buys path', () => { - let browser; - let page; - - beforeAll(async() => { - browser = await getBrowser(); - page = browser.page; - await page.loginAndModule('buyer', 'entry'); - await page.accessToSearchResult('1'); - }); - - afterAll(async() => { - await browser.close(); - }); - - it('should count the summary buys and find there only one at this point', async() => { - const buysCount = await page.countElement(selectors.entrySummary.anyBuyLine); - - expect(buysCount).toEqual(1); - }); - - it('should navigate to the buy section and then click the import button opening the import form', async() => { - await page.accessToSection('entry.card.buy.index'); - await page.waitToClick(selectors.entryBuys.importButton); - await page.waitForState('entry.card.buy.import'); - }); - - it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => { - await page.write(selectors.entryBuys.ref, 'a reference'); - await page.write(selectors.entryBuys.observation, 'an observation'); - - let currentDir = process.cwd(); - let filePath = `${currentDir}/e2e/assets/07_import_buys.json`; - - const [fileChooser] = await Promise.all([ - page.waitForFileChooser(), - page.waitToClick(selectors.entryBuys.file) - ]); - await fileChooser.accept([filePath]); - - await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm'); - await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m'); - await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m'); - await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Container ammo box 1m'); - - await page.waitToClick(selectors.entryBuys.importBuysButton); - - const message = await page.waitForSnackbar(); - const state = await page.getState(); - - expect(message.text).toContain('Data saved!'); - expect(state).toBe('entry.card.buy.index'); - }); - - it('should navigate to the entry summary and count the buys to find 4 buys have been added', async() => { - await page.waitToClick('vn-icon[icon="preview"]'); - await page.waitForNumberOfElements(selectors.entrySummary.anyBuyLine, 5); - }); -}); diff --git a/front/core/components/chip/style.scss b/front/core/components/chip/style.scss index 34ee947ba..f6e4a388f 100644 --- a/front/core/components/chip/style.scss +++ b/front/core/components/chip/style.scss @@ -21,6 +21,9 @@ vn-chip { } } + &.transparent { + background-color: transparent; + } &.colored { background-color: $color-main; color: $color-font-bg; diff --git a/modules/entry/back/methods/entry/addBuy.js b/modules/entry/back/methods/entry/addBuy.js new file mode 100644 index 000000000..a7d2f4646 --- /dev/null +++ b/modules/entry/back/methods/entry/addBuy.js @@ -0,0 +1,165 @@ + +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; + +module.exports = Self => { + Self.remoteMethodCtx('addBuy', { + description: 'Inserts a new buy for the current entry', + accessType: 'WRITE', + accepts: [{ + arg: 'id', + type: 'number', + required: true, + description: 'The entry id', + http: {source: 'path'} + }, + { + arg: 'itemFk', + type: 'number', + required: true + }, + { + arg: 'quantity', + type: 'number', + required: true + }, + { + arg: 'packageFk', + type: 'string', + required: true + }, + { + arg: 'packing', + type: 'number', + }, + { + arg: 'grouping', + type: 'number' + }, + { + arg: 'weight', + type: 'number', + }, + { + arg: 'stickers', + type: 'number', + }, + { + arg: 'price2', + type: 'number', + }, + { + arg: 'price3', + type: 'number', + }, + { + arg: 'buyingValue', + type: 'number' + }], + returns: { + type: 'object', + root: true + }, + http: { + path: `/:id/addBuy`, + verb: 'POST' + } + }); + + Self.addBuy = async(ctx, options) => { + const conn = Self.dataSource.connector; + let tx; + let myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const models = Self.app.models; + + ctx.args.entryFk = ctx.args.id; + + // remove unwanted properties + delete ctx.args.id; + delete ctx.args.ctx; + + const newBuy = await models.Buy.create(ctx.args, myOptions); + + let filter = { + fields: [ + 'id', + 'itemFk', + 'stickers', + 'packing', + 'grouping', + 'quantity', + 'packageFk', + 'weight', + 'buyingValue', + 'price2', + 'price3' + ], + include: { + relation: 'item', + scope: { + fields: [ + 'id', + 'typeFk', + 'name', + 'size', + 'minPrice', + 'tag5', + 'value5', + 'tag6', + 'value6', + 'tag7', + 'value7', + 'tag8', + 'value8', + 'tag9', + 'value9', + 'tag10', + 'value10', + 'groupingMode' + ], + include: { + relation: 'itemType', + scope: { + fields: ['code', 'description'] + } + } + } + } + }; + + let stmts = []; + let stmt; + + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc'); + stmt = new ParameterizedSQL( + `CREATE TEMPORARY TABLE tmp.buyRecalc + (INDEX (id)) + ENGINE = MEMORY + SELECT ? AS id`, [newBuy.id]); + + stmts.push(stmt); + stmts.push('CALL buy_recalcPrices()'); + + const sql = ParameterizedSQL.join(stmts, ';'); + await conn.executeStmt(sql, myOptions); + + const buy = await models.Buy.findById(newBuy.id, filter, myOptions); + + if (tx) await tx.commit(); + + return buy; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/entry/back/methods/entry/deleteBuys.js b/modules/entry/back/methods/entry/deleteBuys.js new file mode 100644 index 000000000..de038d66e --- /dev/null +++ b/modules/entry/back/methods/entry/deleteBuys.js @@ -0,0 +1,50 @@ +module.exports = Self => { + Self.remoteMethodCtx('deleteBuys', { + description: 'Deletes the selected buys', + accessType: 'WRITE', + accepts: [{ + arg: 'buys', + type: ['object'], + required: true, + description: 'The buys to delete' + }], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/deleteBuys`, + verb: 'POST' + } + }); + + Self.deleteBuys = async(ctx, options) => { + const models = Self.app.models; + let tx; + let myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + let promises = []; + for (let buy of ctx.args.buys) { + const buysToDelete = models.Buy.destroyById(buy.id, myOptions); + promises.push(buysToDelete); + } + + const deleted = await Promise.all(promises); + + if (tx) await tx.commit(); + return deleted; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/entry/back/methods/entry/specs/addBuy.spec.js b/modules/entry/back/methods/entry/specs/addBuy.spec.js new file mode 100644 index 000000000..ef1177ed5 --- /dev/null +++ b/modules/entry/back/methods/entry/specs/addBuy.spec.js @@ -0,0 +1,42 @@ +const app = require('vn-loopback/server/server'); +const LoopBackContext = require('loopback-context'); + +describe('entry addBuy()', () => { + const activeCtx = { + accessToken: {userId: 18}, + }; + + const ctx = { + req: activeCtx + }; + + const entryId = 2; + it('should create a new buy for the given entry', async() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + const itemId = 4; + const quantity = 10; + + ctx.args = { + id: entryId, + itemFk: itemId, + quantity: quantity, + packageFk: 3 + }; + + const tx = await app.models.Entry.beginTransaction({}); + const options = {transaction: tx}; + + try { + const newBuy = await app.models.Entry.addBuy(ctx, options); + + expect(newBuy.itemFk).toEqual(itemId); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/entry/back/methods/entry/specs/deleteBuys.spec.js b/modules/entry/back/methods/entry/specs/deleteBuys.spec.js new file mode 100644 index 000000000..b937b7474 --- /dev/null +++ b/modules/entry/back/methods/entry/specs/deleteBuys.spec.js @@ -0,0 +1,33 @@ +const app = require('vn-loopback/server/server'); +const LoopBackContext = require('loopback-context'); + +describe('sale deleteBuys()', () => { + const activeCtx = { + accessToken: {userId: 18}, + }; + + const ctx = { + req: activeCtx + }; + + it('should delete the buy', async() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + + const tx = await app.models.Entry.beginTransaction({}); + const options = {transaction: tx}; + + ctx.args = {buys: [{id: 1}]}; + + try { + const result = await app.models.Buy.deleteBuys(ctx, options); + + expect(result).toEqual([{count: 1}]); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/entry/back/models/buy.js b/modules/entry/back/models/buy.js index e110164e8..34f19e765 100644 --- a/modules/entry/back/models/buy.js +++ b/modules/entry/back/models/buy.js @@ -1,4 +1,5 @@ module.exports = Self => { require('../methods/entry/editLatestBuys')(Self); require('../methods/entry/latestBuysFilter')(Self); + require('../methods/entry/deleteBuys')(Self); }; diff --git a/modules/entry/back/models/buy.json b/modules/entry/back/models/buy.json index 65bf15fa4..8e36d0eef 100644 --- a/modules/entry/back/models/buy.json +++ b/modules/entry/back/models/buy.json @@ -57,14 +57,12 @@ "entry": { "type": "belongsTo", "model": "Entry", - "foreignKey": "entryFk", - "required": true + "foreignKey": "entryFk" }, "item": { "type": "belongsTo", "model": "Item", - "foreignKey": "itemFk", - "required": true + "foreignKey": "itemFk" }, "package": { "type": "belongsTo", diff --git a/modules/entry/back/models/entry.js b/modules/entry/back/models/entry.js index f1a22fddd..88611963f 100644 --- a/modules/entry/back/models/entry.js +++ b/modules/entry/back/models/entry.js @@ -2,6 +2,7 @@ module.exports = Self => { require('../methods/entry/filter')(Self); require('../methods/entry/getEntry')(Self); require('../methods/entry/getBuys')(Self); + require('../methods/entry/addBuy')(Self); require('../methods/entry/importBuys')(Self); require('../methods/entry/importBuysPreview')(Self); }; diff --git a/modules/entry/front/buy/import/index.js b/modules/entry/front/buy/import/index.js index 9dc3418f0..b5ff92a89 100644 --- a/modules/entry/front/buy/import/index.js +++ b/modules/entry/front/buy/import/index.js @@ -55,7 +55,7 @@ class Controller extends Section { fetchBuys(buys) { const params = {buys}; - const query = `Entries/${this.entry.id}/importBuysPreview`; + const query = `Entries/${this.$params.id}/importBuysPreview`; this.$http.get(query, {params}).then(res => { this.import.buys = res.data; }); @@ -71,7 +71,7 @@ class Controller extends Section { if (hasAnyEmptyRow) throw new Error(`Some of the imported buys doesn't have an item`); - const query = `Entries/${this.entry.id}/importBuys`; + const query = `Entries/${this.$params.id}/importBuys`; return this.$http.post(query, params) .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))) .then(() => this.$state.go('entry.card.buy.index')); diff --git a/modules/entry/front/buy/import/index.spec.js b/modules/entry/front/buy/import/index.spec.js index 126c7375f..c948304c7 100644 --- a/modules/entry/front/buy/import/index.spec.js +++ b/modules/entry/front/buy/import/index.spec.js @@ -16,6 +16,7 @@ describe('Entry', () => { controller.entry = { id: 1 }; + controller.$params = {id: 1}; })); describe('fillData()', () => { diff --git a/modules/entry/front/buy/index/index.html b/modules/entry/front/buy/index/index.html index 0ff11c8a6..22ae27540 100644 --- a/modules/entry/front/buy/index/index.html +++ b/modules/entry/front/buy/index/index.html @@ -1,9 +1,198 @@ + + + + +
+ + + + + + + +

Subtotal {{$ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}

+

VAT {{$ctrl.ticket.totalWithVat - $ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}

+

Total {{$ctrl.ticket.totalWithVat | currency: 'EUR':2}}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + ItemQuantityPackageStickersWeightPackingGroupingBuying valueGrouping pricePacking priceImport
+ + + + + {{::buy.item.id | zeroFill:6}} + + + + {{::id}} - {{::name}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{buy.quantity * buy.buyingValue | currency: 'EUR':2}} + +
+ + + {{::buy.item.itemType.code}} + + + + {{::buy.item.size}} + + + + {{::buy.item.minPrice | currency: 'EUR':2}} + + + {{::buy.item.name}} + + +
+
+ + +
+
+
+ vn-bind="+"> -
\ No newline at end of file + + + + + + \ No newline at end of file diff --git a/modules/entry/front/buy/index/index.js b/modules/entry/front/buy/index/index.js index 6cb27e022..97c89a437 100644 --- a/modules/entry/front/buy/index/index.js +++ b/modules/entry/front/buy/index/index.js @@ -1,9 +1,66 @@ import ngModule from '../../module'; +import './style.scss'; import Section from 'salix/components/section'; +export default class Controller extends Section { + saveBuy(buy) { + const missingData = !buy.itemFk || !buy.quantity || !buy.packageFk; + if (missingData) return; + + let options; + if (buy.id) { + options = { + query: `Buys/${buy.id}`, + method: 'patch' + }; + } else { + options = { + query: `Entries/${this.entry.id}/addBuy`, + method: 'post' + }; + } + this.$http[options.method](options.query, buy).then(res => { + if (!res.data) return; + + buy = Object.assign(buy, res.data); + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } + + /** + * Returns checked instances + * + * @return {Array} Checked instances + */ + selectedBuys() { + if (!this.buys) return; + + return this.buys.filter(buy => { + return buy.checked; + }); + } + + deleteBuys() { + const buys = this.selectedBuys(); + const actualInstances = buys.filter(buy => buy.id); + + const params = {buys: actualInstances}; + + if (actualInstances.length) { + this.$http.post(`Buys/deleteBuys`, params).then(() => { + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } + buys.forEach(buy => { + const index = this.buys.indexOf(buy); + this.buys.splice(index, 1); + }); + } +} + ngModule.vnComponent('vnEntryBuyIndex', { template: require('./index.html'), - controller: Section, + controller: Controller, bindings: { entry: '<' } diff --git a/modules/entry/front/buy/index/index.spec.js b/modules/entry/front/buy/index/index.spec.js new file mode 100644 index 000000000..9e9318d10 --- /dev/null +++ b/modules/entry/front/buy/index/index.spec.js @@ -0,0 +1,66 @@ +import './index.js'; + +describe('Entry buy', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('entry')); + + beforeEach(angular.mock.inject(($componentController, $compile, $rootScope, _$httpParamSerializer_, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + let $element = $compile(' { + it(`should call the buys patch route if the received buy has an ID`, () => { + const buy = {id: 1, itemFk: 1, quantity: 1, packageFk: 1}; + + const query = `Buys/${buy.id}`; + + $httpBackend.expectPATCH(query).respond(200); + controller.saveBuy(buy); + $httpBackend.flush(); + }); + + it(`should call the entry addBuy post route if the received buy has no ID`, () => { + controller.entry = {id: 1}; + const buy = {itemFk: 1, quantity: 1, packageFk: 1}; + + const query = `Entries/${controller.entry.id}/addBuy`; + + $httpBackend.expectPOST(query).respond(200); + controller.saveBuy(buy); + $httpBackend.flush(); + }); + }); + + describe('deleteBuys()', () => { + it(`should perform no queries if all buys to delete were not actual instances`, () => { + controller.buys = [ + {checked: true}, + {checked: true}, + {checked: false}]; + + controller.deleteBuys(); + + expect(controller.buys.length).toEqual(1); + }); + + it(`should perform a query to delete as there's an actual instance at least`, () => { + controller.buys = [ + {checked: true, id: 1}, + {checked: true}, + {checked: false}]; + + const query = 'Buys/deleteBuys'; + + $httpBackend.expectPOST(query).respond(200); + controller.deleteBuys(); + $httpBackend.flush(); + + expect(controller.buys.length).toEqual(1); + }); + }); +}); diff --git a/modules/entry/front/buy/index/locale/es.yml b/modules/entry/front/buy/index/locale/es.yml index 8f2be1e44..bd10b39aa 100644 --- a/modules/entry/front/buy/index/locale/es.yml +++ b/modules/entry/front/buy/index/locale/es.yml @@ -1 +1,2 @@ -Buy: Lineas de entrada \ No newline at end of file +Buys: Compras +Delete buy(s): Eliminar compra(s) \ No newline at end of file diff --git a/modules/entry/front/buy/index/style.scss b/modules/entry/front/buy/index/style.scss new file mode 100644 index 000000000..836f1eddc --- /dev/null +++ b/modules/entry/front/buy/index/style.scss @@ -0,0 +1,28 @@ +@import "variables"; + + +vn-entry-buy-index vn-card { + max-width: $width-xl; + + .dark-row { + background-color: lighten($color-marginal, 10%); + } + + tbody tr:nth-child(1) { + border-top: 1px solid $color-marginal; + } + + tbody{ + border-bottom: 1px solid $color-marginal; + } + + tbody tr:nth-child(3) { + height: inherit + } + + tr { + margin-bottom: 10px; + } +} + +$color-font-link-medium: lighten($color-font-link, 20%) \ No newline at end of file diff --git a/modules/entry/front/latest-buys/index.html b/modules/entry/front/latest-buys/index.html index d34dfdbb2..277b1b347 100644 --- a/modules/entry/front/latest-buys/index.html +++ b/modules/entry/front/latest-buys/index.html @@ -83,12 +83,12 @@ - + {{::buy.packing | dashIfEmpty}} - + {{::buy.grouping | dashIfEmpty}} diff --git a/modules/entry/front/routes.json b/modules/entry/front/routes.json index 34e6f469b..0dc1e7ea0 100644 --- a/modules/entry/front/routes.json +++ b/modules/entry/front/routes.json @@ -87,13 +87,14 @@ "url": "/buy", "state": "entry.card.buy", "abstract": true, - "component": "ui-view" + "component": "ui-view", + "acl": ["buyer"] }, { "url" : "/index", "state": "entry.card.buy.index", "component": "vn-entry-buy-index", - "description": "Buy", + "description": "Buys", "params": { "entry": "$ctrl.entry" }, diff --git a/modules/entry/front/summary/index.html b/modules/entry/front/summary/index.html index 2843ecc46..24cedcdb2 100644 --- a/modules/entry/front/summary/index.html +++ b/modules/entry/front/summary/index.html @@ -10,67 +10,67 @@ - - - - - - + - {{$ctrl.entryData.travel.agency.name}} - - - - @@ -102,12 +102,12 @@ {{::line.packageFk | dashIfEmpty}} {{::line.weight}} - + {{::line.packing | dashIfEmpty}} - + {{::line.grouping | dashIfEmpty}} diff --git a/modules/ticket/front/sale/index.html b/modules/ticket/front/sale/index.html index 778e46001..d4dfe1a8a 100644 --- a/modules/ticket/front/sale/index.html +++ b/modules/ticket/front/sale/index.html @@ -91,81 +91,81 @@ vn-tooltip="{{::$ctrl.$t('Reserved')}}"> - - - - - - {{sale.itemFk}} - - - - {{::id}} - {{::name}} - - - - - {{sale.quantity}} - - + + + + + {{sale.itemFk}} + + - - - - - - {{sale.concept}} - -

{{::sale.subName}}

-
- - -
- - - - -
- - - {{sale.price | currency: 'EUR':2}} - - - - - {{(sale.discount / 100) | percentage}} - - - - {{$ctrl.getSaleTotal(sale) | currency: 'EUR':2}} - - + url="Items" + ng-model="sale.itemFk" + show-field="name" + value-field="id" + search-function="$ctrl.itemSearchFunc($search)" + on-change="$ctrl.changeQuantity(sale)" + order="id DESC" + tabindex="1"> + + {{::id}} - {{::name}} + + + + + {{sale.quantity}} + + + + + + + + {{sale.concept}} + +

{{::sale.subName}}

+
+ + +
+ + + + +
+ + + {{sale.price | currency: 'EUR':2}} + + + + + {{(sale.discount / 100) | percentage}} + + + + {{$ctrl.getSaleTotal(sale) | currency: 'EUR':2}} + +